From c9b50e7db652195d6926f081edf935cb44b5b838 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 May 2022 11:52:10 +0530 Subject: [PATCH 1/7] feat: timer in quiz --- lms/lms/doctype/lms_quiz/lms_quiz.json | 19 ++++- lms/plugins.py | 20 ++++- lms/public/css/style.css | 5 +- lms/templates/quiz.html | 55 +++++++++---- lms/www/batch/learn.js | 104 +++++++++++++++++-------- 5 files changed, 150 insertions(+), 53 deletions(-) diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json index 129291a4..e68caf67 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.json +++ b/lms/lms/doctype/lms_quiz/lms_quiz.json @@ -10,7 +10,9 @@ "field_order": [ "title", "questions", - "lesson" + "lesson", + "max_attempts", + "time" ], "fields": [ { @@ -31,11 +33,23 @@ "label": "Lesson", "options": "Course Lesson", "read_only": 1 + }, + { + "default": "0", + "fieldname": "max_attempts", + "fieldtype": "Int", + "label": "Max Attempts" + }, + { + "default": "0", + "fieldname": "time", + "fieldtype": "Int", + "label": "Time Per Question (in Seconds)" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-30 13:10:06.929358", + "modified": "2022-05-16 14:47:55.364743", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz", @@ -57,5 +71,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lms/plugins.py b/lms/plugins.py index cd03d2a6..354fe281 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -103,7 +103,25 @@ def set_mandatory_fields_for_profile(): def quiz_renderer(quiz_name): quiz = frappe.get_doc("LMS Quiz", quiz_name) - context = dict(quiz=quiz) + + context = { + "quiz": quiz + } + + no_of_attempts = frappe.db.count("LMS Quiz Submission", { + "owner": frappe.session.user, + "quiz": quiz_name}) + + if quiz.max_attempts and no_of_attempts >= quiz.max_attempts: + last_attempt_score = frappe.db.get_value("LMS Quiz Submission", { + "owner": frappe.session.user, + "quiz": quiz_name + }, ["score"]) + + context.update({ + "attempts_exceeded": True, + "last_attempt_score": last_attempt_score + }) return frappe.render_template("templates/quiz.html", context) def exercise_renderer(argument): diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 235ee522..09432a8e 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -575,7 +575,7 @@ input[type=checkbox] { } .lesson-content-card { - margin-top: 1rem; + margin: 3rem 0; } .lesson-page { @@ -1342,8 +1342,7 @@ pre { font-size: var(--text-lg); color: var(--gray-900); font-weight: 600; - flex-grow: 1; - margin-left: 1rem; + width: 75%; } .profile-page-body { diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 1e68d995..f0d585e4 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -1,18 +1,34 @@ +{% if attempts_exceeded %} +
+
{{ quiz.title }}
+
{{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }}
+
{{ _("You latest score is {0}.").format(last_attempt_score) }}
+
+{% else %}
{{ quiz.title }}
-
+
+
{{ quiz.title }}
+
{{ _("This quiz has {0} questions.").format(quiz.questions | length) }}
+ {% if quiz.time %} +
{{ _("This is a time bound quiz. You will have {0} seconds per question.").format(quiz.time) }}
+ {% endif %} +
{{ _("Start") }}
+
+ +
{% for question in quiz.questions %} {% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %} -
+
-
{{ loop.index }}
-
-
{{ instruction }}
- {{ frappe.utils.md_to_html(question.question) }} -
+
{{ loop.index }}
+
+ {{ frappe.utils.md_to_html(question.question) }} +
+
{{ instruction }}
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} @@ -40,15 +56,26 @@
{% endfor %}
+ +

+{% endif %} diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index b44d5af9..131aacb7 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -45,6 +45,12 @@ frappe.ready(() => { clear_work(e); }); + $(".btn-start-quiz").click((e) => { + $("#start-banner").addClass("hide"); + $("#quiz-form").removeClass("hide"); + mark_active_question(); + }) + }); const save_current_lesson = () => { @@ -65,19 +71,19 @@ const enable_check = (e) => { }; const mark_active_question = (e = undefined) => { - var current_index; - var next_index = 1; - if (e) { - e.preventDefault(); - current_index = $(".active-question").attr("data-qt-index"); - next_index = parseInt(current_index) + 1; - } - $(".question").addClass("hide").removeClass("active-question"); - $(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question"); - $(".current-question").text(`${next_index}`); - $("#check").removeClass("hide").attr("disabled", true); - $("#next").addClass("hide"); - $(".explanation").addClass("hide"); + $(".timer").addClass("hide"); + calculate_and_display_time(100); + $(".timer").removeClass("hide"); + + let current_index = $(".active-question").attr("data-qt-index") || 0; + let next_index = parseInt(current_index) + 1; + $(".question").addClass("hide").removeClass("active-question"); + $(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question"); + $(".current-question").text(`${next_index}`); + $("#check").removeClass("hide").attr("disabled", true); + $("#next").addClass("hide"); + $(".explanation").addClass("hide"); + initialize_timer(); }; const mark_progress = (e) => { @@ -85,7 +91,7 @@ const mark_progress = (e) => { if ($(e.currentTarget).prop("nodeName") != "INPUT") e.preventDefault(); else - return + return; const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress"); const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete"; @@ -179,30 +185,30 @@ const try_quiz_again = (e) => { window.location.reload(); }; -const check_answer = (e) => { - e.preventDefault(); +const check_answer = (e=undefined) => { + e && e.preventDefault(); + clearInterval(self.timer); + $(".timer").addClass("hide"); + var quiz_name = $("#quiz-title").text(); + var total_questions = $(".question").length; + var current_index = $(".active-question").attr("data-qt-index"); - var quiz_name = $("#quiz-title").text(); - var total_questions = $(".question").length; - var current_index = $(".active-question").attr("data-qt-index"); + $(".explanation").removeClass("hide"); + $("#check").addClass("hide"); - $(".explanation").removeClass("hide"); - $("#check").addClass("hide"); - - if (current_index == total_questions) { - if ($(".eligible-for-submission").length) { - $("#summary").removeClass("hide") + if (current_index == total_questions) { + if ($(".eligible-for-submission").length) { + $("#summary").removeClass("hide"); + } + else { + $("#submission-message").removeClass("hide"); + } } else { - $("#submission-message").removeClass("hide"); + $("#next").removeClass("hide"); } - } - else { - $("#next").removeClass("hide") - } - - var [answer, is_correct] = parse_options(); - add_to_local_storage(quiz_name, current_index, answer, is_correct) + var [answer, is_correct] = parse_options(); + add_to_local_storage(quiz_name, current_index, answer, is_correct); }; const parse_options = () => { @@ -381,3 +387,35 @@ const fetch_assignments = () => { } }); }; + +const initialize_timer = () => { + this.time_left = $(".timer").data("time"); + calculate_and_display_time(100, this.time_left); + $(".timer").removeClass("hide"); + const total_time = $(".timer").data("time"); + this.start_time = new Date().getTime(); + const self = this; + let old_diff; + + this.timer = setInterval(function () { + var diff = (new Date().getTime() - self.start_time)/1000; + var variation = old_diff ? diff - old_diff : diff; + old_diff = diff; + self.time_left -= variation; + let percent_time = (self.time_left / total_time) * 100; + calculate_and_display_time(percent_time); + if (self.time_left <= 0) { + clearInterval(self.timer); + $(".timer").addClass("hide"); + check_answer(); + } + }, 100); +}; + +const calculate_and_display_time = (percent_time) => { + $(".timer .progress-bar").attr("aria-valuenow", percent_time); + $(".timer .progress-bar").attr("aria-valuemax", percent_time); + $(".timer .progress-bar").css("width", `${percent_time}%`); + let progress_color = percent_time < 20 ? "red" : "var(--primary-color)"; + $(".timer .progress-bar").css("background-color", progress_color); +}; From 5ebf7b7992fe244565489f1fa438f52c765677da Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 May 2022 15:31:05 +0530 Subject: [PATCH 2/7] fix: start banner style --- lms/templates/quiz.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index f0d585e4..7a163bee 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -9,11 +9,20 @@
-
{{ quiz.title }}
-
{{ _("This quiz has {0} questions.").format(quiz.questions | length) }}
- {% if quiz.time %} -
{{ _("This is a time bound quiz. You will have {0} seconds per question.").format(quiz.time) }}
- {% endif %} +
{{ quiz.title }}
+
+ {{ _("This quiz has {0} questions.").format(quiz.questions | length) }} + + {% if quiz.max_attempts %} + {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} + {{ _("You can attempt this quiz {0} {1}.").format(quiz.max_attempts, suffix) }} + {% endif %} + + {% if quiz.time %} + {{ _("The quiz is time bound. You will have {0} seconds per question.").format(quiz.time) }} + {% endif %} +
+
{{ _("Start") }}
From a460ea5194b0d626ba98532939cc9c2b5e3fe9f1 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 May 2022 16:04:03 +0530 Subject: [PATCH 3/7] fix: start banner style --- lms/templates/quiz.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 7a163bee..8ac0abbd 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -12,12 +12,10 @@
{{ quiz.title }}
{{ _("This quiz has {0} questions.").format(quiz.questions | length) }} - {% if quiz.max_attempts %} {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} {{ _("You can attempt this quiz {0} {1}.").format(quiz.max_attempts, suffix) }} {% endif %} - {% if quiz.time %} {{ _("The quiz is time bound. You will have {0} seconds per question.").format(quiz.time) }} {% endif %} From 1ef70dd4e49336a5bc355196f1d978d4adab93ce Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 May 2022 17:12:54 +0530 Subject: [PATCH 4/7] fix: submit quiz if user leaves page --- lms/templates/quiz.html | 12 +++++----- lms/www/batch/learn.js | 50 ++++++++++++++++++++++++----------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 8ac0abbd..093bceda 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -1,23 +1,25 @@ {% if attempts_exceeded %}
-
{{ quiz.title }}
+
{{ quiz.title }}
{{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }}
{{ _("You latest score is {0}.").format(last_attempt_score) }}
{% else %} -
{{ quiz.title }}
+
{{ quiz.title }}
{{ quiz.title }}
- {{ _("This quiz has {0} questions.").format(quiz.questions | length) }} + {{ _("There are {0} questions in this quiz.").format(quiz.questions | length) }} + {% if quiz.max_attempts %} {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} - {{ _("You can attempt this quiz {0} {1}.").format(quiz.max_attempts, suffix) }} + {{ _("This quiz can only be taken {0} {1}. If you attempt the quiz and leave the page before submitting, the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }} {% endif %} + {% if quiz.time %} - {{ _("The quiz is time bound. You will have {0} seconds per question.").format(quiz.time) }} + {{ _("The quiz has a time limit. Each question will be given {0} seconds.").format(quiz.time) }} {% endif %}
diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 131aacb7..21089e50 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -49,8 +49,16 @@ frappe.ready(() => { $("#start-banner").addClass("hide"); $("#quiz-form").removeClass("hide"); mark_active_question(); - }) + }); + if ($("#quiz-form").length) { + window.addEventListener("beforeunload", (e) => { + e.preventDefault(); + if ($("#quiz-title").data("max-attempts") && $(".active-question").length) + quiz_summary(); + e.returnValue = ''; + }); + } }); const save_current_lesson = () => { @@ -158,27 +166,27 @@ const move_to_next_lesson = (status, e) => { } }; -const quiz_summary = (e) => { - e.preventDefault(); - var quiz_name = $("#quiz-title").text(); - var total_questions = $(".question").length; +const quiz_summary = (e=undefined) => { + e && e.preventDefault(); + var quiz_name = $("#quiz-title").text(); + var total_questions = $(".question").length; - frappe.call({ - method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary", - args: { - "quiz": quiz_name, - "results": localStorage.getItem(quiz_name) - }, - callback: (data) => { - var message = data.message == total_questions ? "Excellent Work" : "You were almost there." - $(".question").addClass("hide"); - $("#summary").addClass("hide"); - $("#quiz-form").parent().prepend( - `

${message} 👏

-
${data.message}/${total_questions} correct.
`); - $("#try-again").removeClass("hide"); - } - }) + frappe.call({ + method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary", + args: { + "quiz": quiz_name, + "results": localStorage.getItem(quiz_name) + }, + callback: (data) => { + var message = data.message == total_questions ? "Excellent Work" : "You were almost there." + $(".question").addClass("hide"); + $("#summary").addClass("hide"); + $("#quiz-form").parent().prepend( + `

${message} 👏

+
${data.message}/${total_questions} correct.
`); + $("#try-again").removeClass("hide"); + } + }) }; const try_quiz_again = (e) => { From cd56fc6179a3dd04d83863a4994676e82db09e15 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 May 2022 17:29:11 +0530 Subject: [PATCH 5/7] fix: banner style and page exit condition --- lms/templates/quiz.html | 18 +++++------------- lms/www/batch/learn.js | 5 ++--- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 093bceda..47dc65ef 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -9,21 +9,13 @@
-
{{ quiz.title }}
-
- {{ _("There are {0} questions in this quiz.").format(quiz.questions | length) }} - - {% if quiz.max_attempts %} - {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} - {{ _("This quiz can only be taken {0} {1}. If you attempt the quiz and leave the page before submitting, the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }} - {% endif %} - - {% if quiz.time %} - {{ _("The quiz has a time limit. Each question will be given {0} seconds.").format(quiz.time) }} - {% endif %} +
{{ quiz.title }}
+
{{ _("There are {0} questions in this quiz.").format(quiz.questions | length) }}{% if quiz.max_attempts %} + {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} {{ _("This quiz can only be taken {0} {1}. If you attempt the quiz and leave the page before submitting, the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }}{% endif %} + {% if quiz.time %}{{ _("The quiz has a time limit. Each question will be given {0} seconds.").format(quiz.time) }}{% endif %}
-
{{ _("Start") }}
+
diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 21089e50..387e9319 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -51,11 +51,10 @@ frappe.ready(() => { mark_active_question(); }); - if ($("#quiz-form").length) { + if ($("#quiz-title").data("max-attempts") && $(".active-question").length) { window.addEventListener("beforeunload", (e) => { e.preventDefault(); - if ($("#quiz-title").data("max-attempts") && $(".active-question").length) - quiz_summary(); + quiz_summary(); e.returnValue = ''; }); } From e465ad58caf4ed8c622d9a7fc9275bad4f862fef Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 May 2022 17:47:27 +0530 Subject: [PATCH 6/7] fix: removed unnecessary tags --- lms/templates/quiz.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 47dc65ef..882bf135 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -74,9 +74,6 @@ {{ _("Please join the course to submit the Quiz.") }}
{{ _("Try Again") }}
- -

-
{% endif %} From af21d33165997dc6514bd739a7667a9162470a77 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 18 May 2022 10:47:49 +0530 Subject: [PATCH 7/7] fix: page reload condition --- lms/lms/doctype/lms_quiz/lms_quiz.py | 7 ++++--- lms/templates/quiz.html | 2 +- lms/www/batch/learn.js | 14 ++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index dd0b4809..098fa7e4 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -46,13 +46,14 @@ class LMSQuiz(Document): @frappe.whitelist() def quiz_summary(quiz, results): score = 0 - results = json.loads(results) + results = results and json.loads(results) for result in results: correct = result["is_correct"][0] result["question"] = frappe.db.get_value("LMS Quiz Question", - {"parent": quiz, "idx": result["question_index"]}, - ["question"]) + {"parent": quiz, + "idx": result["question_index"]}, + ["question"]) for point in result["is_correct"]: correct = correct and point diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 882bf135..8674f1ce 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -2,7 +2,7 @@
{{ quiz.title }}
{{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }}
-
{{ _("You latest score is {0}.").format(last_attempt_score) }}
+
{{ _("Your latest score is {0}.").format(last_attempt_score) }}
{% else %}
{{ quiz.title }}
diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 387e9319..dc533e7d 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -51,11 +51,10 @@ frappe.ready(() => { mark_active_question(); }); - if ($("#quiz-title").data("max-attempts") && $(".active-question").length) { + if ($("#quiz-title").data("max-attempts")) { window.addEventListener("beforeunload", (e) => { - e.preventDefault(); - quiz_summary(); - e.returnValue = ''; + e.returnValue = ""; + $(".active-question").length && quiz_summary(); }); } }); @@ -169,7 +168,6 @@ const quiz_summary = (e=undefined) => { e && e.preventDefault(); var quiz_name = $("#quiz-title").text(); var total_questions = $(".question").length; - frappe.call({ method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary", args: { @@ -177,12 +175,12 @@ const quiz_summary = (e=undefined) => { "results": localStorage.getItem(quiz_name) }, callback: (data) => { - var message = data.message == total_questions ? "Excellent Work" : "You were almost there." + var message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time") $(".question").addClass("hide"); $("#summary").addClass("hide"); $("#quiz-form").parent().prepend( - `

${message} 👏

-
${data.message}/${total_questions} correct.
`); + `

${message}

+
${data.message}/${total_questions}
`); $("#try-again").removeClass("hide"); } })