diff --git a/community/lms/doctype/lms_quiz/lms_quiz.py b/community/lms/doctype/lms_quiz/lms_quiz.py index de002046..b1fe65a9 100644 --- a/community/lms/doctype/lms_quiz/lms_quiz.py +++ b/community/lms/doctype/lms_quiz/lms_quiz.py @@ -46,34 +46,30 @@ class LMSQuiz(Document): return result[0] @frappe.whitelist() -def submit(quiz, result): +def quiz_summary(quiz, results): 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) + results = json.loads(results) - for response in result: - match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0] - correct_options = quiz_details.get_correct_options(match) - correct_answers = [ match.get(answer_map[option]) for option in correct_options ] + 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"]) - 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")) ]) + for point in result["is_correct"]: + correct = correct and point + + result["result"] = "Right" if correct else "Wrong" + score += correct + + del result["is_correct"] + del result["question_index"] frappe.get_doc({ "doctype": "LMS Quiz Submission", "quiz": quiz, - "result": result, + "result": results, "score": score }).save(ignore_permissions=True) - update_progress(quiz_details.lesson) + return score diff --git a/community/lms/doctype/lms_quiz/test_lms_quiz.py b/community/lms/doctype/lms_quiz/test_lms_quiz.py index e2df8fda..77ca72c3 100644 --- a/community/lms/doctype/lms_quiz/test_lms_quiz.py +++ b/community/lms/doctype/lms_quiz/test_lms_quiz.py @@ -3,6 +3,39 @@ # import frappe import unittest +import frappe class TestLMSQuiz(unittest.TestCase): - pass + + @classmethod + def setUpClass(cls) -> None: + frappe.get_doc({ + "doctype": "LMS Quiz", + "title": "Test Quiz" + }).save() + + def test_with_multiple_options(self): + quiz = frappe.get_doc("LMS Quiz", "Test Quiz") + quiz.append("questions", { + "question": "Question multiple", + "option_1": "Option 1", + "is_correct_1": 1, + "option_2": "Option 2", + "is_correct_2": 1 + }) + quiz.save() + self.assertTrue(quiz.questions[0].multiple) + + def test_with_no_correct_option(self): + quiz = frappe.get_doc("LMS Quiz", "Test Quiz") + quiz.append("questions", { + "question": "Question no correct option", + "option_1": "Option 1", + "option_2": "Option 2", + }) + self.assertRaises(frappe.ValidationError, quiz.save) + + @classmethod + def tearDownClass(cls) -> None: + frappe.db.delete("LMS Quiz", "Test Quiz") + frappe.db.delete("LMS Quiz Question", {"parent": "Test Quiz"}) diff --git a/community/public/css/style.css b/community/public/css/style.css index b2b98732..6c6e6a9f 100644 --- a/community/public/css/style.css +++ b/community/public/css/style.css @@ -333,7 +333,7 @@ input[type=checkbox] { .card-divider { border: 1px solid #F4F5F6; - margin-bottom: 16px; + margin-bottom: 1rem; } .card-divider-dark { @@ -487,23 +487,30 @@ input[type=checkbox] { --star-fill: #74808B; } -div.custom-checkbox>label>input { +.custom-checkbox { + display: flex; + align-items: center; +} + +.custom-checkbox>label>input { visibility: hidden; } -div.custom-checkbox>label>img { - height: 20px; - width: 20px; +.custom-checkbox>label>.empty-checkbox { + height: 1.5rem; + width: 1.5rem; border: 1px solid black; border-radius: 5px; } -div.custom-checkbox>label>input:checked+img { - background: url(/assets/community/images/Vector.png); +.custom-checkbox>label>input:checked+.empty-checkbox { + background: url(/assets/community/icons/tick.svg); background-repeat: no-repeat; background-position: center center; - background-size: 15px 15px; - object-fit: contain; +} + +.quiz-label { + margin-bottom: 0; } .course-card-wide { @@ -1013,8 +1020,24 @@ div.custom-checkbox>label>input:checked+img { color: red; } +.quiz-footer { + display: flex; + align-items: center; + justify-content: space-between; +} + .question { flex-direction: column; + width: 688px; + margin: auto; +} + +.question p { + margin-bottom: 0; +} + +.active-question .card-divider { + margin-top: 1rem; } .dark-links { @@ -1057,7 +1080,7 @@ div.custom-checkbox>label>input:checked+img { } .course-content-parent .course-home-headings { - margin: 0px 0px 16px; + margin: 0px 0px 1rem; } .lesson-pagination { diff --git a/community/public/icons/minus-circle-green.svg b/community/public/icons/minus-circle-green.svg new file mode 100644 index 00000000..b1715e62 --- /dev/null +++ b/community/public/icons/minus-circle-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/community/public/icons/minus-circle.svg b/community/public/icons/minus-circle.svg new file mode 100644 index 00000000..3ec77aab --- /dev/null +++ b/community/public/icons/minus-circle.svg @@ -0,0 +1 @@ + diff --git a/community/public/icons/tick.svg b/community/public/icons/tick.svg index 2275a8d4..1c209899 100644 --- a/community/public/icons/tick.svg +++ b/community/public/icons/tick.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/community/public/icons/wrong.svg b/community/public/icons/wrong.svg new file mode 100644 index 00000000..454f1c3d --- /dev/null +++ b/community/public/icons/wrong.svg @@ -0,0 +1,3 @@ + + + diff --git a/community/templates/quiz.html b/community/templates/quiz.html index c2d5e251..64457e47 100644 --- a/community/templates/quiz.html +++ b/community/templates/quiz.html @@ -1,33 +1,59 @@ -{% set last_submission = quiz.get_last_submission_details() %} -{% if last_submission %} -
-
Last Submitted On: {{ frappe.utils.pretty_date(last_submission.creation) }}
-
Last Submission Score: {{ last_submission.score }}
-
-{% endif %} -
{{ quiz.title }}
-
- {% for question in quiz.questions %} -
-

{{ loop.index }}. {{ question.question }}

- {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} - {% for option in options %} - {% if option %} -
- - {{ option }} +
{{ quiz.title }}
+ +
+ +
+ +
+ {% for question in quiz.questions %} +
+

{{ question.question }}

+ + {% if question.multiple %} + Choose all answers that apply: + {% else %} + Choose 1 answer: + {% endif %} + +
+ + {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} + + {% for option in options %} + {% if option %} + +
+ + {{ frappe.utils.md_to_html(option) }} +
+ + {% set explanation = question['explanation_' + loop.index | string] %} + {% if explanation %} + {{ explanation }} + {% endif %} + +
+ {% endif %} + {% endfor %} + +
+ {% endfor %}
- {% endif %} - {% endfor %} -
- {% endfor %} - - -

-
- + +
Try Again
+

+
+ +
diff --git a/community/www/batch/learn.js b/community/www/batch/learn.js index e9898843..27bd9dca 100644 --- a/community/www/batch/learn.js +++ b/community/www/batch/learn.js @@ -1,13 +1,27 @@ frappe.ready(() => { + localStorage.removeItem($("#quiz-title").text()); + save_current_lesson(); + $(".option").click((e) => { + enable_check(e); + }) + $("#progress").click((e) => { mark_progress(e); }); - $("#submit-quiz").click((e) => { - submit_quiz(e); + $("#summary").click((e) => { + quiz_summary(e); + }); + + $("#check").click((e) => { + check_answer(e); + }); + + $("#next").click((e) => { + mark_active_question(e); }); $("#try-again").click((e) => { @@ -25,6 +39,27 @@ var save_current_lesson = () => { } } +var enable_check = (e) => { + if ($(".option:checked").length && $("#check").attr("disabled")) { + $("#check").removeAttr("disabled"); + } +} + +var 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"); +} + var mark_progress = (e) => { var status = $(e.currentTarget).attr("data-progress"); frappe.call({ @@ -56,47 +91,77 @@ var change_progress_indicators = (status, e) => { $(e.currentTarget).text(label).attr("data-progress", data_progress); } -var submit_quiz = (e) => { +var quiz_summary = (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(decodeURIComponent(elem.value))); - result.push({ - "question": element.dataset.question, - "answer": answers - }); - }); + var quiz_name = $("#quiz-title").text(); + var total_questions = $(".question").length; + frappe.call({ - method: "community.lms.doctype.lms_quiz.lms_quiz.submit", + method: "community.lms.doctype.lms_quiz.lms_quiz.quiz_summary", args: { - quiz: $("#title").text(), - result: result + "quiz": quiz_name, + "results": localStorage.getItem(quiz_name) }, callback: (data) => { - $("#submit-quiz").addClass("hide"); + var message = data.message == total_questions ? "Excellent Work" : "You were almost there." + $(".question").addClass("hide"); + $(".quiz-footer").addClass("hide"); + $("#quiz-form").parent().prepend( + `

${message} 👏

+
${data.message}/${total_questions} correct.
`); $("#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}`); } }) } var try_quiz_again = (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"); + window.location.reload(); +} + +var check_answer = (e) => { + e.preventDefault(); + + 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"); + current_index == total_questions ? $("#summary").removeClass("hide") : $("#next").removeClass("hide"); + + var [answer, is_correct] = parse_options(); + add_to_local_storage(quiz_name, current_index, answer, is_correct) +} + +var parse_options = () => { + var answer = []; + var is_correct = []; + $(".active-question input").each((i, element) => { + var correct = parseInt($(element).attr("data-correct")); + if ($(element).prop("checked")) { + answer.push(decodeURIComponent($(element).val())); + correct && is_correct.push(1); + correct ? add_icon(element, "check") : add_icon(element, "wrong"); + } + else { + correct && is_correct.push(0); + correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle"); + } + }) + return [answer, is_correct]; +} + +var add_icon = (element, icon) => { + $(element).parent().empty().html(``); +} + +var add_to_local_storage = (quiz_name, current_index, answer, is_correct) => { + var quiz_stored = JSON.parse(localStorage.getItem(quiz_name)); + var quiz_obj = { + "question_index": current_index, + "answer": answer.join(), + "is_correct": is_correct + } + quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj] + localStorage.setItem(quiz_name, JSON.stringify(quiz_stored)) }