fix: quiz progress and youtube video integration
This commit is contained in:
@@ -161,10 +161,10 @@ whitelist = [
|
||||
"/socket.io",
|
||||
"/hackathons",
|
||||
"/dashboard",
|
||||
"/join-request"
|
||||
"/join-request",
|
||||
"/add-a-new-batch",
|
||||
"/new-sign-up",
|
||||
"/message"
|
||||
"/message",
|
||||
"/about"
|
||||
]
|
||||
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
||||
@@ -188,4 +188,8 @@ update_website_context = 'community.widgets.update_website_context'
|
||||
# community_lesson_page_extension = None
|
||||
|
||||
## Markdown Macros for Lessons
|
||||
# community_markdown_macro_renderers = {"Exercise": "myapp.mymodule.plugins.render_exercise"}
|
||||
community_markdown_macro_renderers = {
|
||||
"Exercise": "community.plugins.exercise_renderer",
|
||||
"Quiz": "community.plugins.quiz_renderer",
|
||||
"YouTubeVideo": "community.plugins.youtube_video_renderer",
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"chapter",
|
||||
"lesson_type",
|
||||
"include_in_preview",
|
||||
"column_break_4",
|
||||
"title",
|
||||
@@ -24,14 +23,6 @@
|
||||
"label": "Chapter",
|
||||
"options": "Chapter"
|
||||
},
|
||||
{
|
||||
"default": "Video",
|
||||
"fieldname": "lesson_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Lesson Type",
|
||||
"options": "Video\nText\nQuiz"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
@@ -73,7 +64,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-11 19:03:23.138165",
|
||||
"modified": "2021-06-23 17:59:52.946515",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Lesson",
|
||||
@@ -95,4 +86,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -9,27 +9,35 @@ from ...md import markdown_to_html, find_macros
|
||||
|
||||
class Lesson(Document):
|
||||
def before_save(self):
|
||||
macros = find_macros(self.body)
|
||||
exercises = [value for name, value in macros if name == "Exercise"]
|
||||
dynamic_documents = ["Exercise", "Quiz"]
|
||||
for section in dynamic_documents:
|
||||
self.update_lesson_name_in_document(section)
|
||||
|
||||
def update_lesson_name_in_document(self, section):
|
||||
doctype_map= {
|
||||
"Exercise": "Exercise",
|
||||
"Quiz": "LMS Quiz"
|
||||
}
|
||||
macros = find_macros(self.body)
|
||||
documents = [value for name, value in macros if name == section]
|
||||
index = 1
|
||||
for name in exercises:
|
||||
e = frappe.get_doc("Exercise", name)
|
||||
for name in documents:
|
||||
e = frappe.get_doc(doctype_map[section], name)
|
||||
e.lesson = self.name
|
||||
e.index_ = index
|
||||
e.save()
|
||||
index += 1
|
||||
self.update_orphan_exercises(exercises)
|
||||
self.update_orphan_documents(doctype_map[section], documents)
|
||||
|
||||
def update_orphan_exercises(self, active_exercises):
|
||||
"""Updates the exercises that were previously part of this lesson,
|
||||
def update_orphan_documents(self, doctype, documents):
|
||||
"""Updates the documents that were previously part of this lesson,
|
||||
but not any more.
|
||||
"""
|
||||
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})}
|
||||
active_exercises = set(active_exercises)
|
||||
orphan_exercises = linked_exercises - active_exercises
|
||||
for name in orphan_exercises:
|
||||
ex = frappe.get_doc("Exercise", name)
|
||||
linked_documents = {row['name'] for row in frappe.get_all(doctype, {"lesson": self.name})}
|
||||
active_documents = set(documents)
|
||||
orphan_documents = linked_documents - active_documents
|
||||
for name in orphan_documents:
|
||||
ex = frappe.get_doc(doctype, name)
|
||||
ex.lesson = None
|
||||
ex.index_ = 0
|
||||
ex.index_label = ""
|
||||
@@ -92,13 +100,30 @@ def update_progress(lesson):
|
||||
course_progress.save(ignore_permissions=True)
|
||||
|
||||
def all_dynamic_content_submitted(lesson, user):
|
||||
all_exercises_submitted = check_all_exercise_submission(lesson, user)
|
||||
all_quiz_submitted = check_all_quiz_submitted(lesson, user)
|
||||
return all_exercises_submitted and all_quiz_submitted
|
||||
|
||||
def check_all_exercise_submission(lesson, user):
|
||||
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True)
|
||||
all_exercises_submitted = False
|
||||
if not len(exercise_names):
|
||||
return True
|
||||
query = {
|
||||
"exercise": ["in", exercise_names],
|
||||
"owner": user
|
||||
}
|
||||
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
|
||||
all_exercises_submitted = True
|
||||
return True
|
||||
return False
|
||||
|
||||
return all_exercises_submitted
|
||||
def check_all_quiz_submitted(lesson, user):
|
||||
quizzes = frappe.get_list("LMS Quiz", {"lesson": lesson}, pluck="name", ignore_permissions=True)
|
||||
if not len(quizzes):
|
||||
return True
|
||||
query = {
|
||||
"quiz": ["in", quizzes],
|
||||
"owner": user
|
||||
}
|
||||
if frappe.db.count("LMS Quiz Submission", query) == len(quizzes):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -199,13 +199,15 @@ class LMSCourse(Document):
|
||||
}
|
||||
if batch:
|
||||
filters["batch"] = batch
|
||||
return frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True)
|
||||
membership = frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True)
|
||||
if membership and membership.batch:
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
return membership
|
||||
|
||||
def get_all_memberships(self, member=frappe.session.user):
|
||||
def get_all_memberships(self, member):
|
||||
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
|
||||
for membership in all_memberships:
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
print(all_memberships)
|
||||
return all_memberships
|
||||
|
||||
def get_mentors(self, batch=None):
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"questions"
|
||||
"questions",
|
||||
"lesson"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -21,11 +22,17 @@
|
||||
"fieldtype": "Table",
|
||||
"label": "Questions",
|
||||
"options": "LMS Quiz Question"
|
||||
},
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lesson",
|
||||
"options": "Lesson"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-07 12:22:37.333289",
|
||||
"modified": "2021-06-23 17:58:57.642873",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz",
|
||||
|
||||
@@ -1,36 +1,79 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from community.lms.doctype.lesson.lesson import update_progress
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
import json
|
||||
from frappe import _
|
||||
from ..lesson.lesson import update_progress
|
||||
|
||||
class LMSQuiz(Document):
|
||||
pass
|
||||
def validate(self):
|
||||
self.validate_correct_answers()
|
||||
|
||||
def validate_correct_answers(self):
|
||||
for question in self.questions:
|
||||
correct_options = self.get_correct_options(question)
|
||||
|
||||
if len(correct_options) > 1:
|
||||
question.multiple = 1
|
||||
|
||||
if not len(correct_options):
|
||||
frappe.throw(_("At least one answer must be correct for this question: {0}").format(frappe.bold(question.question)))
|
||||
|
||||
def get_correct_options(self, question):
|
||||
correct_option_fields = ["is_correct_1", "is_correct_2", "is_correct_3", "is_correct_4"]
|
||||
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
|
||||
|
||||
def get_last_submission_details(self):
|
||||
"""Returns the latest submission for this user.
|
||||
"""
|
||||
user = frappe.session.user
|
||||
if not user or user == "Guest":
|
||||
return
|
||||
|
||||
result = frappe.get_all('LMS Quiz Submission',
|
||||
fields="*",
|
||||
filters={
|
||||
"owner": user,
|
||||
"quiz": self.name
|
||||
},
|
||||
order_by="creation desc",
|
||||
page_length=1)
|
||||
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit(quiz, result):
|
||||
score = 0
|
||||
answer_map = {
|
||||
"is_correct_1": "option_1",
|
||||
"is_correct_2": "option_2",
|
||||
"is_correct_3": "option_3",
|
||||
"is_correct_4": "option_4"
|
||||
}
|
||||
result = json.loads(result)
|
||||
quiz_details = frappe.get_doc("LMS Quiz", quiz)
|
||||
print(result, type(result))
|
||||
|
||||
for response in result:
|
||||
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0]
|
||||
response["users_response"] = ("").join([ ans for ans in response.get("answer") ]).replace(" ", ", ")
|
||||
del response["answer"]
|
||||
print(response.get("users_response"), match.answer)
|
||||
if response.get("users_response") == match.answer:
|
||||
correct_options = quiz_details.get_correct_options(match)
|
||||
correct_answers = [ match.get(answer_map[option]) for option in correct_options ]
|
||||
|
||||
if response.get("answer") == correct_answers:
|
||||
response["result"] = "Right"
|
||||
score += 1
|
||||
else:
|
||||
response["result"] = "Wrong"
|
||||
response["answer"] = ("").join([ ans if idx == len(response.get("answer")) -1 else ans + ", " for idx, ans in enumerate(response.get("answer")) ])
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "LMS Quiz Submission",
|
||||
"quiz": quiz,
|
||||
"result": result,
|
||||
"score": score
|
||||
}).save()
|
||||
}).save(ignore_permissions=True)
|
||||
update_progress(quiz_details.lesson)
|
||||
return score
|
||||
|
||||
|
||||
|
||||
@@ -6,41 +6,107 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"question",
|
||||
"options",
|
||||
"answer",
|
||||
"multiple_correct_answers"
|
||||
"options_section",
|
||||
"option_1",
|
||||
"is_correct_1",
|
||||
"section_break_5",
|
||||
"option_2",
|
||||
"is_correct_2",
|
||||
"column_break_4",
|
||||
"option_3",
|
||||
"is_correct_3",
|
||||
"section_break_11",
|
||||
"option_4",
|
||||
"is_correct_4",
|
||||
"multiple"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "question",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Question"
|
||||
"label": "Question",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options"
|
||||
"fieldname": "option_1",
|
||||
"fieldtype": "Data",
|
||||
"label": "Option 1",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "option_2",
|
||||
"fieldtype": "Data",
|
||||
"label": "Option 2",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "option_3",
|
||||
"fieldtype": "Data",
|
||||
"label": "Option 3"
|
||||
},
|
||||
{
|
||||
"fieldname": "option_4",
|
||||
"fieldtype": "Data",
|
||||
"label": "Option 4"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "multiple_correct_answers",
|
||||
"depends_on": "option_1",
|
||||
"fieldname": "is_correct_1",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Multiple Correct Answers"
|
||||
"label": "Is Correct"
|
||||
},
|
||||
{
|
||||
"fieldname": "answer",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Answer"
|
||||
"default": "0",
|
||||
"depends_on": "option_2",
|
||||
"fieldname": "is_correct_2",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Correct"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "option_3",
|
||||
"fieldname": "is_correct_3",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Correct"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "option_4",
|
||||
"fieldname": "is_correct_4",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Correct"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "multiple",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Multiple Correct Answers",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "options_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-07 12:47:22.787160",
|
||||
"modified": "2021-06-22 16:54:13.133859",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Question",
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-06-07 10:48:57.994714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"question",
|
||||
"options",
|
||||
"multiple_correct_answers"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "question",
|
||||
"fieldtype": "Data",
|
||||
"label": "Question"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Table",
|
||||
"label": "Options",
|
||||
"options": "LMS Option"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "multiple_correct_answers",
|
||||
"fieldtype": "Check",
|
||||
"label": "Multiple Correct Answers"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-07 10:48:57.994714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Questions",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class LMSQuizQuestions(Document):
|
||||
pass
|
||||
@@ -6,7 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"question",
|
||||
"users_response",
|
||||
"answer",
|
||||
"result"
|
||||
],
|
||||
"fields": [
|
||||
@@ -16,24 +16,24 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Question"
|
||||
},
|
||||
{
|
||||
"fieldname": "users_response",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Users Response"
|
||||
},
|
||||
{
|
||||
"fieldname": "result",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Result",
|
||||
"options": "Right\nWrong"
|
||||
},
|
||||
{
|
||||
"fieldname": "answer",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Users Response"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-07 14:21:07.768039",
|
||||
"modified": "2021-06-22 18:32:28.813159",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Result",
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a
|
||||
class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
|
||||
{{ course.title }}</span> {% endif %}
|
||||
{% set all_memberships = course.get_all_memberships() %}
|
||||
{% if all_memberships | length > 1 %}
|
||||
<a class="nav-link pull-right" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
Switch Batch
|
||||
{% set all_memberships = course.get_all_memberships(frappe.session.user) %}
|
||||
{% if membership and membership.batch and all_memberships | length > 1 %}
|
||||
<a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ membership.batch_title }}
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
{% for data in all_memberships %}
|
||||
{% if data.batch != membership.batch %}
|
||||
<a class="dropdown-item switch-batch" href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
|
||||
<a class="dropdown-item switch-batch"
|
||||
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -28,7 +29,8 @@
|
||||
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson else '1.1' %}
|
||||
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson
|
||||
else '1.1' %}
|
||||
<a class="nav-link" id="learn"
|
||||
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
|
||||
</li>
|
||||
|
||||
@@ -7,9 +7,17 @@
|
||||
<div class="chapter-lessons">
|
||||
{% for lesson in chapter.get_lessons() %}
|
||||
<div class="lesson-teaser">
|
||||
<a {% if show_link or lesson.include_in_preview %}
|
||||
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}" {% else %} href="" class="no-preview"
|
||||
{% endif %} data-course="{{ course.name }}">{{ lesson.title }}</a>
|
||||
{% if show_link or lesson.include_in_preview %}
|
||||
<a class="" href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
|
||||
data-course="{{ course.name }}">{{ lesson.title }}</a>
|
||||
{% else %}
|
||||
<div class="no-preview" title="This lesson is not available for preview">
|
||||
<span style="color: #2490ef;">
|
||||
{{ lesson.title }}
|
||||
</span>
|
||||
<i class="fa fa-lock ml-2"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
|
||||
<span class="ml-5 badge p-2 {{ lesson.get_slugified_class() }}"> {{ lesson.get_progress() }}</span>
|
||||
{% endif %}
|
||||
@@ -18,36 +26,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
var d;
|
||||
$(".no-preview").click((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
var message = __("Please enroll for this course to access the lesson.");
|
||||
var label = __("Checkout Upcoming Batches");
|
||||
var action = "checkout_upcoming_batches";
|
||||
d = frappe.msgprint({
|
||||
title: __("This lesson is not available for preview!"),
|
||||
message: message,
|
||||
primary_action: {
|
||||
"label": label,
|
||||
"client_action": action,
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
window.redirect_to_login = () => {
|
||||
window.location.href = `/login?redirect-to=/courses/${$(".no-preview").attr("data-course")}`
|
||||
}
|
||||
|
||||
window.checkout_upcoming_batches = () => {
|
||||
if ($(".upcoming").length > 0) {
|
||||
$('html,body').animate({ scrollTop: $(".upcoming").offset().top }, 300);
|
||||
}
|
||||
frappe.hide_msgprint();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<h3 id="title">{{ quiz.title }}</h3>
|
||||
<form id="quiz-form">
|
||||
{% for question in quiz.questions %}
|
||||
<div class="question mb-5" data-question="{{ question.question }}"
|
||||
data-multi="{{ question.multiple_correct_answers}}">
|
||||
<p> {{ loop.index }}. {{ question.question }}</p>
|
||||
{% set options = question.options.split(",") %}
|
||||
{% for option in options %}
|
||||
<div class="checkbox">
|
||||
<input {% if question.multiple_correct_answers %} type="checkbox" {% else %} type="radio"
|
||||
name="{{ question.question | urlencode }}" {% endif %} class="option" value="{{ option }}">
|
||||
<span class="label-area">{{ option }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-secondary hide mb-5" id="try-again">Try Again</button>
|
||||
<button class="btn btn-primary" id="submit">Submit</button>
|
||||
<h4 class="success-message"></h4>
|
||||
<h5 class="score text-muted"></h5>
|
||||
</form>
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$("#submit").click((e) => {
|
||||
e.preventDefault();
|
||||
var result = [];
|
||||
$('.question').each((i, element) => {
|
||||
var options = $(element).find(".option");
|
||||
var answers = [];
|
||||
options.filter((i, op) => $(op).prop("checked")).each((i, elem) => answers.push(elem.value));
|
||||
result.push({
|
||||
"question": element.dataset.question,
|
||||
"answer": answers
|
||||
});
|
||||
});
|
||||
frappe.call({
|
||||
method: "community.lms.doctype.lms_quiz.lms_quiz.submit",
|
||||
args: {
|
||||
quiz: $("#title").text(),
|
||||
result: result
|
||||
},
|
||||
callback: (data) => {
|
||||
$("#submit").addClass("hide");
|
||||
$("#try-again").removeClass("hide");
|
||||
$(":input[type='checkbox']").prop("disabled", true);
|
||||
$(":input[type='radio']").prop("disabled", true);
|
||||
if (data.message == result.length) {
|
||||
$(".success-message").text("Congratulations, you cleared the quiz!");
|
||||
}
|
||||
else {
|
||||
$(".success-message").text("Some of your answers weren't correct. You can give it another shot.");
|
||||
}
|
||||
$(".score").text(`Score: ${data.message}/${result.length}`);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$("#try-again").click((e) => {
|
||||
e.preventDefault();
|
||||
$("#quiz-form").trigger("reset");
|
||||
$(".success-message").text("");
|
||||
$(".score").text("");
|
||||
$("#submit").removeClass("hide");
|
||||
$("#try-again").addClass("hide");
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -14,6 +14,8 @@ The PageExtension is used to load additinal stylesheets and scripts to
|
||||
be loaded in a webpage.
|
||||
"""
|
||||
|
||||
import frappe
|
||||
|
||||
class PageExtension:
|
||||
"""PageExtension is a plugin to inject custom styles and scripts
|
||||
into a web page.
|
||||
@@ -64,3 +66,24 @@ class ProfileTab:
|
||||
Every subclass must implement this.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def quiz_renderer(quiz_name):
|
||||
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
||||
context = dict(quiz=quiz)
|
||||
return frappe.render_template("templates/quiz.html", context)
|
||||
|
||||
def exercise_renderer(argument):
|
||||
exercise = frappe.get_doc("Exercise", argument)
|
||||
context = dict(exercise=exercise)
|
||||
return frappe.render_template("templates/exercise.html", context)
|
||||
|
||||
def youtube_video_renderer(video_id):
|
||||
return f"""
|
||||
<iframe width="560" height="315"
|
||||
src="https://www.youtube.com/embed/{video_id}"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
"""
|
||||
|
||||
@@ -76,11 +76,6 @@ h2 {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.no-preview:hover {
|
||||
cursor: pointer;
|
||||
color: #2490ef;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 60px 0px;
|
||||
}
|
||||
|
||||
10
community/templates/exercise.html
Normal file
10
community/templates/exercise.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="exercise">
|
||||
<h3>Exercise {{exercise.index_label}}: {{ exercise.title }}</h3>
|
||||
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
|
||||
|
||||
{% set submission = exercise.get_user_submission() %}
|
||||
|
||||
<pre class="exercise" id="exercise-{{exercise.name}}"
|
||||
data-last-submitted="{{ submission and submission.creation or '' }}" data-name="{{ exercise.name }}"
|
||||
data-image='{{ (submission and submission.image or "") | tojson }}'><code class="language-joy">{{ submission.solution if submission else exercise.code }}</code></pre>
|
||||
</div>
|
||||
30
community/templates/quiz.html
Normal file
30
community/templates/quiz.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% set last_submission = quiz.get_last_submission_details() %}
|
||||
{% if last_submission %}
|
||||
<div class="mb-5 pull-right">
|
||||
<div class="text-muted">Last Submitted On: {{ frappe.utils.pretty_date(last_submission.creation) }}</div>
|
||||
<div class="text-muted">Last Submission Score: {{ last_submission.score }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3 id="title" class="mb-5">{{ quiz.title }}</h3>
|
||||
<form id="quiz-form">
|
||||
{% for question in quiz.questions %}
|
||||
<div class="question mb-5" data-question="{{ question.question }}"
|
||||
data-multi="{{ question.multiple_correct_answers}}">
|
||||
<p> {{ loop.index }}. {{ question.question }}</p>
|
||||
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
||||
{% for option in options %}
|
||||
{% if option %}
|
||||
<div class="checkbox mb-2">
|
||||
<input {% if question.multiple %} type="checkbox" {% else %} type="radio"
|
||||
name="{{ question.question | urlencode }}" {% endif %} class="option" value="{{ option | urlencode }}">
|
||||
<span class="label-area">{{ option }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-secondary hide mb-5" id="try-again">Try Again</button>
|
||||
<button class="btn btn-primary" id="submit-quiz">Submit</button>
|
||||
<h4 class="success-message"></h4>
|
||||
<h5 class="score text-muted"></h5>
|
||||
</form>
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="container mt-5">
|
||||
{{ widgets.BatchTabs(course=course, membership=membership) }}
|
||||
<div class="course-details mt-5">
|
||||
{{ widgets.CourseOutline(course=course, batch=batch, show_link=True, show_progress=True) }}
|
||||
{{ widgets.CourseOutline(course=course, batch=batch, show_link=membership, show_progress=True) }}
|
||||
</div>
|
||||
|
||||
{% if batch %}
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
<a href="/courses/{{ course.name }}">Checkout Course Details.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if membership %}
|
||||
{{ pagination(prev_chap, prev_url, next_chap, next_url) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
/* Save Lesson Progress */
|
||||
if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) {
|
||||
frappe.call({
|
||||
method: "community.lms.doctype.lesson.lesson.save_progress",
|
||||
@@ -8,10 +10,60 @@ frappe.ready(() => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Save Current Lesson */
|
||||
if ($(".title").attr("data-membership")) {
|
||||
frappe.call("community.lms.api.save_current_lesson", {
|
||||
course_name: $(".title").attr("data-course"),
|
||||
lesson_name: $(".title").attr("data-lesson")
|
||||
})
|
||||
}
|
||||
|
||||
/* Submit Quiz */
|
||||
$("#submit-quiz").click((e) => {
|
||||
e.preventDefault();
|
||||
console.log("click")
|
||||
var result = [];
|
||||
$('.question').each((i, element) => {
|
||||
var options = $(element).find(".option");
|
||||
var answers = [];
|
||||
options.filter((i, op) => $(op).prop("checked")).each((i, elem) => answers.push(decodeURIComponent(elem.value)));
|
||||
result.push({
|
||||
"question": element.dataset.question,
|
||||
"answer": answers
|
||||
});
|
||||
});
|
||||
frappe.call({
|
||||
method: "community.lms.doctype.lms_quiz.lms_quiz.submit",
|
||||
args: {
|
||||
quiz: $("#title").text(),
|
||||
result: result
|
||||
},
|
||||
callback: (data) => {
|
||||
$("#submit-quiz").addClass("hide");
|
||||
$("#try-again").removeClass("hide");
|
||||
$(":input[type='checkbox']").prop("disabled", true);
|
||||
$(":input[type='radio']").prop("disabled", true);
|
||||
if (data.message == result.length) {
|
||||
$(".success-message").text("Congratulations, you cleared the quiz!");
|
||||
}
|
||||
else {
|
||||
$(".success-message").text("Some of your answers weren't correct. You can give it another shot.");
|
||||
}
|
||||
$(".score").text(`Score: ${data.message}/${result.length}`);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/* Try the quiz again */
|
||||
$("#try-again").click((e) => {
|
||||
e.preventDefault();
|
||||
$(":input[type='checkbox']").prop("disabled", false);
|
||||
$(":input[type='radio']").prop("disabled", false);
|
||||
$("#quiz-form").trigger("reset");
|
||||
$(".success-message").text("");
|
||||
$(".score").text("");
|
||||
$("#submit-quiz").removeClass("hide");
|
||||
$("#try-again").addClass("hide");
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ def get_common_context(context):
|
||||
context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch)
|
||||
context.member_count = len(context.members)
|
||||
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else " "
|
||||
context.livecode_url = get_livecode_url()
|
||||
|
||||
def get_livecode_url():
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="Courses {{course.title}}" />
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -30,7 +31,7 @@
|
||||
{{ CourseDescription(course) }}
|
||||
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
|
||||
{{ BatchSection(course) }}
|
||||
{{ widgets.CourseOutline(course=course, show_link=False) }}
|
||||
{{ widgets.CourseOutline(course=course, show_link=membership) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,9 @@ def get_context(context):
|
||||
raise frappe.Redirect
|
||||
|
||||
context.course = course
|
||||
if not course.is_mentor(frappe.session.user):
|
||||
batch = course.get_membership(frappe.session.user)
|
||||
if batch:
|
||||
frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
|
||||
raise frappe.Redirect
|
||||
membership = course.get_membership(frappe.session.user)
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.membership = membership
|
||||
if not course.is_mentor(frappe.session.user) and membership:
|
||||
frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
|
||||
raise frappe.Redirect
|
||||
|
||||
Reference in New Issue
Block a user