fix: quiz progress and youtube video integration

This commit is contained in:
pateljannat
2021-06-24 10:25:23 +05:30
parent 5d96bf544d
commit 0284c9305c
24 changed files with 352 additions and 242 deletions

View File

@@ -161,10 +161,10 @@ whitelist = [
"/socket.io", "/socket.io",
"/hackathons", "/hackathons",
"/dashboard", "/dashboard",
"/join-request" "/join-request",
"/add-a-new-batch", "/add-a-new-batch",
"/new-sign-up", "/new-sign-up",
"/message" "/message",
"/about" "/about"
] ]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist] 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 # community_lesson_page_extension = None
## Markdown Macros for Lessons ## 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",
}

View File

@@ -7,7 +7,6 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"chapter", "chapter",
"lesson_type",
"include_in_preview", "include_in_preview",
"column_break_4", "column_break_4",
"title", "title",
@@ -24,14 +23,6 @@
"label": "Chapter", "label": "Chapter",
"options": "Chapter" "options": "Chapter"
}, },
{
"default": "Video",
"fieldname": "lesson_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Lesson Type",
"options": "Video\nText\nQuiz"
},
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
@@ -73,7 +64,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-11 19:03:23.138165", "modified": "2021-06-23 17:59:52.946515",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",
@@ -95,4 +86,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -9,27 +9,35 @@ from ...md import markdown_to_html, find_macros
class Lesson(Document): class Lesson(Document):
def before_save(self): def before_save(self):
macros = find_macros(self.body) dynamic_documents = ["Exercise", "Quiz"]
exercises = [value for name, value in macros if name == "Exercise"] 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 index = 1
for name in exercises: for name in documents:
e = frappe.get_doc("Exercise", name) e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name e.lesson = self.name
e.index_ = index e.index_ = index
e.save() e.save()
index += 1 index += 1
self.update_orphan_exercises(exercises) self.update_orphan_documents(doctype_map[section], documents)
def update_orphan_exercises(self, active_exercises): def update_orphan_documents(self, doctype, documents):
"""Updates the exercises that were previously part of this lesson, """Updates the documents that were previously part of this lesson,
but not any more. but not any more.
""" """
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})} linked_documents = {row['name'] for row in frappe.get_all(doctype, {"lesson": self.name})}
active_exercises = set(active_exercises) active_documents = set(documents)
orphan_exercises = linked_exercises - active_exercises orphan_documents = linked_documents - active_documents
for name in orphan_exercises: for name in orphan_documents:
ex = frappe.get_doc("Exercise", name) ex = frappe.get_doc(doctype, name)
ex.lesson = None ex.lesson = None
ex.index_ = 0 ex.index_ = 0
ex.index_label = "" ex.index_label = ""
@@ -92,13 +100,30 @@ def update_progress(lesson):
course_progress.save(ignore_permissions=True) course_progress.save(ignore_permissions=True)
def all_dynamic_content_submitted(lesson, user): 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) 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 = { query = {
"exercise": ["in", exercise_names], "exercise": ["in", exercise_names],
"owner": user "owner": user
} }
if frappe.db.count("Exercise Submission", query) == len(exercise_names): 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

View File

@@ -199,13 +199,15 @@ class LMSCourse(Document):
} }
if batch: if batch:
filters["batch"] = 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"]) all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
for membership in all_memberships: for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
print(all_memberships)
return all_memberships return all_memberships
def get_mentors(self, batch=None): def get_mentors(self, batch=None):

View File

@@ -7,7 +7,8 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "title",
"questions" "questions",
"lesson"
], ],
"fields": [ "fields": [
{ {
@@ -21,11 +22,17 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Questions", "label": "Questions",
"options": "LMS Quiz Question" "options": "LMS Quiz Question"
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-06-07 12:22:37.333289", "modified": "2021-06-23 17:58:57.642873",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",

View File

@@ -1,36 +1,79 @@
# Copyright (c) 2021, FOSS United and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
from community.lms.doctype.lesson.lesson import update_progress
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json import json
from frappe import _
from ..lesson.lesson import update_progress
class LMSQuiz(Document): 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() @frappe.whitelist()
def submit(quiz, result): def submit(quiz, result):
score = 0 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) result = json.loads(result)
quiz_details = frappe.get_doc("LMS Quiz", quiz) quiz_details = frappe.get_doc("LMS Quiz", quiz)
print(result, type(result))
for response in result: for response in result:
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0] 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(" ", ", ") correct_options = quiz_details.get_correct_options(match)
del response["answer"] correct_answers = [ match.get(answer_map[option]) for option in correct_options ]
print(response.get("users_response"), match.answer)
if response.get("users_response") == match.answer: if response.get("answer") == correct_answers:
response["result"] = "Right" response["result"] = "Right"
score += 1 score += 1
else: else:
response["result"] = "Wrong" 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({ frappe.get_doc({
"doctype": "LMS Quiz Submission", "doctype": "LMS Quiz Submission",
"quiz": quiz, "quiz": quiz,
"result": result, "result": result,
"score": score "score": score
}).save() }).save(ignore_permissions=True)
update_progress(quiz_details.lesson)
return score return score

View File

@@ -6,41 +6,107 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"options", "options_section",
"answer", "option_1",
"multiple_correct_answers" "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": [ "fields": [
{ {
"fieldname": "question", "fieldname": "question",
"fieldtype": "Text", "fieldtype": "Text",
"in_list_view": 1, "in_list_view": 1,
"label": "Question" "label": "Question",
"reqd": 1
}, },
{ {
"fieldname": "options", "fieldname": "option_1",
"fieldtype": "Text", "fieldtype": "Data",
"in_list_view": 1, "label": "Option 1",
"label": "Options" "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", "default": "0",
"fieldname": "multiple_correct_answers", "depends_on": "option_1",
"fieldname": "is_correct_1",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "label": "Is Correct"
"label": "Multiple Correct Answers"
}, },
{ {
"fieldname": "answer", "default": "0",
"fieldtype": "Data", "depends_on": "option_2",
"in_list_view": 1, "fieldname": "is_correct_2",
"label": "Answer" "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, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-07 12:47:22.787160", "modified": "2021-06-22 16:54:13.133859",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Question", "name": "LMS Quiz Question",

View File

@@ -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
}

View File

@@ -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

View File

@@ -6,7 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"users_response", "answer",
"result" "result"
], ],
"fields": [ "fields": [
@@ -16,24 +16,24 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Question" "label": "Question"
}, },
{
"fieldname": "users_response",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Users Response"
},
{ {
"fieldname": "result", "fieldname": "result",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Result", "label": "Result",
"options": "Right\nWrong" "options": "Right\nWrong"
},
{
"fieldname": "answer",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Users Response"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-07 14:21:07.768039", "modified": "2021-06-22 18:32:28.813159",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Result", "name": "LMS Quiz Result",

View File

@@ -2,16 +2,17 @@
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a <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"> class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
{{ course.title }}</span> {% endif %} {{ course.title }}</span> {% endif %}
{% set all_memberships = course.get_all_memberships() %} {% set all_memberships = course.get_all_memberships(frappe.session.user) %}
{% if all_memberships | length > 1 %} {% if membership and membership.batch and all_memberships | length > 1 %}
<a class="nav-link pull-right" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" <a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
aria-expanded="false"> data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Switch Batch {{ membership.batch_title }}
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% for data in all_memberships %} {% for data in all_memberships %}
{% if data.batch != membership.batch %} {% 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 %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@@ -28,7 +29,8 @@
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a> <a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
</li> </li>
<li class="nav-item"> <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" <a class="nav-link" id="learn"
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a> href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
</li> </li>

View File

@@ -7,9 +7,17 @@
<div class="chapter-lessons"> <div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %} {% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser"> <div class="lesson-teaser">
<a {% if show_link or lesson.include_in_preview %} {% 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" <a class="" href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
{% endif %} data-course="{{ course.name }}">{{ lesson.title }}</a> 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() %} {% 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> <span class="ml-5 badge p-2 {{ lesson.get_slugified_class() }}"> {{ lesson.get_progress() }}</span>
{% endif %} {% endif %}
@@ -18,36 +26,3 @@
</div> </div>
</div> </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>

View File

@@ -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>

View File

@@ -14,6 +14,8 @@ The PageExtension is used to load additinal stylesheets and scripts to
be loaded in a webpage. be loaded in a webpage.
""" """
import frappe
class PageExtension: class PageExtension:
"""PageExtension is a plugin to inject custom styles and scripts """PageExtension is a plugin to inject custom styles and scripts
into a web page. into a web page.
@@ -64,3 +66,24 @@ class ProfileTab:
Every subclass must implement this. Every subclass must implement this.
""" """
raise NotImplementedError() 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>
"""

View File

@@ -76,11 +76,6 @@ h2 {
text-decoration: none text-decoration: none
} }
.no-preview:hover {
cursor: pointer;
color: #2490ef;
}
section { section {
padding: 60px 0px; padding: 60px 0px;
} }

View 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>

View 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>

View File

@@ -11,7 +11,7 @@
<div class="container mt-5"> <div class="container mt-5">
{{ widgets.BatchTabs(course=course, membership=membership) }} {{ widgets.BatchTabs(course=course, membership=membership) }}
<div class="course-details mt-5"> <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> </div>
{% if batch %} {% if batch %}

View File

@@ -36,8 +36,9 @@
<a href="/courses/{{ course.name }}">Checkout Course Details.</a> <a href="/courses/{{ course.name }}">Checkout Course Details.</a>
</div> </div>
{% endif %} {% endif %}
{% if membership %}
{{ pagination(prev_chap, prev_url, next_chap, next_url) }} {{ pagination(prev_chap, prev_url, next_chap, next_url) }}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,6 @@
frappe.ready(() => { frappe.ready(() => {
/* Save Lesson Progress */
if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) { if ($(".title").attr("data-membership") && !$(".title").hasClass("is_mentor")) {
frappe.call({ frappe.call({
method: "community.lms.doctype.lesson.lesson.save_progress", method: "community.lms.doctype.lesson.lesson.save_progress",
@@ -8,10 +10,60 @@ frappe.ready(() => {
} }
}) })
} }
/* Save Current Lesson */
if ($(".title").attr("data-membership")) { if ($(".title").attr("data-membership")) {
frappe.call("community.lms.api.save_current_lesson", { frappe.call("community.lms.api.save_current_lesson", {
course_name: $(".title").attr("data-course"), course_name: $(".title").attr("data-course"),
lesson_name: $(".title").attr("data-lesson") 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");
})
}) })

View File

@@ -27,7 +27,7 @@ def get_common_context(context):
context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch) context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch)
context.member_count = len(context.members) 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() context.livecode_url = get_livecode_url()
def get_livecode_url(): def get_livecode_url():

View File

@@ -4,6 +4,7 @@
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="Courses" />
<meta name="keywords" content="Courses {{course.title}}" /> <meta name="keywords" content="Courses {{course.title}}" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -30,7 +31,7 @@
{{ CourseDescription(course) }} {{ CourseDescription(course) }}
{{ widgets.InstructorSection(instructor=course.get_instructor()) }} {{ widgets.InstructorSection(instructor=course.get_instructor()) }}
{{ BatchSection(course) }} {{ BatchSection(course) }}
{{ widgets.CourseOutline(course=course, show_link=False) }} {{ widgets.CourseOutline(course=course, show_link=membership) }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,8 +16,9 @@ def get_context(context):
raise frappe.Redirect raise frappe.Redirect
context.course = course context.course = course
if not course.is_mentor(frappe.session.user): membership = course.get_membership(frappe.session.user)
batch = course.get_membership(frappe.session.user) context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
if batch: context.membership = membership
frappe.local.flags.redirect_location = f"/courses/{course.name}/learn" if not course.is_mentor(frappe.session.user) and membership:
raise frappe.Redirect frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
raise frappe.Redirect