From 3150cf2510d64d45a703a5681a4e30366ca4190d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 23 Mar 2023 22:22:57 +0530 Subject: [PATCH 1/4] feat: quiz with user input --- lms/lms/doctype/lms_quiz/lms_quiz.py | 76 +++++++++++++++++-- .../lms_quiz_question/lms_quiz_question.json | 59 +++++++++++++- lms/patches.txt | 1 + lms/patches/v0_0/add_question_type.py | 8 ++ lms/templates/quiz.html | 16 +++- lms/www/batch/learn.js | 75 +++++++++++++----- 6 files changed, 200 insertions(+), 35 deletions(-) create mode 100644 lms/patches/v0_0/add_question_type.py diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 26da9246..bc158433 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -21,17 +21,39 @@ class LMSQuiz(Document): def validate_correct_answers(self): for question in self.questions: - correct_options = self.get_correct_options(question) + if question.type == "Choices": + self.validate_correct_options(question) + else: + self.validate_possible_answer(question) - if len(correct_options) > 1: - question.multiple = 1 + def validate_correct_options(self, question): + correct_options = self.get_correct_options(question) - if not len(correct_options): - frappe.throw( - _("At least one option must be correct for this question: {0}").format( - frappe.bold(question.question) - ) + if len(correct_options) > 1: + question.multiple = 1 + + if not len(correct_options): + frappe.throw( + _("At least one option must be correct for this question: {0}").format( + frappe.bold(question.question) ) + ) + + def validate_possible_answer(self, question): + possible_answers_fields = [ + "possibility_1", + "possibility_2", + "possibility_3", + "possibility_4", + ] + possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields)) + + if not len(possible_answers): + frappe.throw( + _("Add at least one possible answer for this question: {0}").format( + frappe.bold(question.question) + ) + ) def get_correct_options(self, question): correct_option_fields = [ @@ -140,3 +162,41 @@ def save_quiz(quiz_title, questions, quiz): question_doc.save(ignore_permissions=True) return doc.name + + +@frappe.whitelist() +def check_answer(question, type, answer): + if type == "Choices": + return check_choice_answers(question, answer) + else: + return check_input_answers(question, answer) + + +def check_choice_answers(question, answer): + fields = [] + for num in range(1, 5): + fields.append(f"option_{cstr(num)}") + fields.append(f"is_correct_{cstr(num)}") + + question_details = frappe.db.get_value( + "LMS Quiz Question", question, fields, as_dict=1 + ) + + for num in range(1, 5): + if question_details[f"option_{num}"] == answer: + return question_details[f"is_correct_{num}"] + return 0 + + +def check_input_answers(question, answer): + fields = [] + for num in range(1, 5): + fields.append(f"possibility_{cstr(num)}") + + question_details = frappe.db.get_value( + "LMS Quiz Question", question, fields, as_dict=1 + ) + for num in range(1, 5): + if question_details[f"possibility_{num}"] == answer: + return 1 + return 0 diff --git a/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json b/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json index 924e696a..09856df7 100644 --- a/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json +++ b/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "question", + "type", "options_section", "option_1", "is_correct_1", @@ -26,6 +27,13 @@ "is_correct_4", "column_break_20", "explanation_4", + "section_break_mnhr", + "possibility_1", + "possibility_3", + "column_break_vnaj", + "possibility_2", + "possibility_4", + "section_break_c1lf", "multiple" ], "fields": [ @@ -40,13 +48,13 @@ "fieldname": "option_1", "fieldtype": "Data", "label": "Option 1", - "reqd": 1 + "mandatory_depends_on": "eval: doc.type == 'Choices'" }, { "fieldname": "option_2", "fieldtype": "Data", "label": "Option 2", - "reqd": 1 + "mandatory_depends_on": "eval: doc.type == 'Choices'" }, { "fieldname": "option_3", @@ -95,18 +103,22 @@ "read_only": 1 }, { + "depends_on": "eval: doc.type == 'Choices'", "fieldname": "options_section", "fieldtype": "Section Break" }, { + "depends_on": "eval: doc.type == 'Choices'", "fieldname": "column_break_4", "fieldtype": "Section Break" }, { + "depends_on": "eval: doc.type == 'Choices'", "fieldname": "section_break_5", "fieldtype": "Section Break" }, { + "depends_on": "eval: doc.type == 'Choices'", "fieldname": "section_break_11", "fieldtype": "Section Break" }, @@ -149,12 +161,52 @@ { "fieldname": "column_break_20", "fieldtype": "Column Break" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "Choices\nUser Input" + }, + { + "depends_on": "eval: doc.type == 'User Input'", + "fieldname": "section_break_mnhr", + "fieldtype": "Section Break" + }, + { + "fieldname": "possibility_1", + "fieldtype": "Small Text", + "label": "Possible Answer 1", + "mandatory_depends_on": "eval: doc.type == 'User Input'" + }, + { + "fieldname": "possibility_2", + "fieldtype": "Small Text", + "label": "Possible Answer 2" + }, + { + "fieldname": "possibility_3", + "fieldtype": "Small Text", + "label": "Possible Answer 3" + }, + { + "fieldname": "possibility_4", + "fieldtype": "Small Text", + "label": "Possible Answer 4" + }, + { + "fieldname": "section_break_c1lf", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_vnaj", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-07-19 19:35:28.446236", + "modified": "2023-03-17 18:22:20.324536", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Question", @@ -162,5 +214,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lms/patches.txt b/lms/patches.txt index f67b6d87..03c7594c 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -48,3 +48,4 @@ lms.patches.v0_0.user_singles_issue #23-11-2022 lms.patches.v0_0.rename_community_to_users #06-01-2023 lms.patches.v0_0.video_embed_link lms.patches.v0_0.rename_exercise_doctype +lms.patches.v0_0.add_question_type \ No newline at end of file diff --git a/lms/patches/v0_0/add_question_type.py b/lms/patches/v0_0/add_question_type.py new file mode 100644 index 00000000..a4411a0a --- /dev/null +++ b/lms/patches/v0_0/add_question_type.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + questions = frappe.get_all("LMS Quiz Question", pluck="name") + + for question in questions: + frappe.db.set_value("LMS Quiz Question", question, "type", "Choices") diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index 8629a2c9..a21ac009 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -46,9 +46,9 @@
{% for question in quiz.questions %} - {% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %} + {% set instruction = _("Choose all answers that apply") if question.type == "Choices" and question.multiple else _("Choose 1 answer") if question.type == "Choices" else _("Enter the correct answer") %} -
{{ loop.index }}.
@@ -58,6 +58,7 @@
{{ instruction }}
+ {% if question.type == "Choices" %} {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} {% for option in options %} {% if option %} @@ -66,8 +67,7 @@
@@ -80,6 +80,14 @@ {% endif %} {% endfor %} + {% else %} +
+
+ +
+
+ {% endif %} +
{% endfor %} diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 3892861a..c70f07eb 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -2,6 +2,8 @@ frappe.ready(() => { this.marked_as_complete = false; this.quiz_submitted = false; this.file_type; + this.answer = []; + this.is_correct = []; let self = this; localStorage.removeItem($("#quiz-title").data("name")); @@ -16,6 +18,10 @@ frappe.ready(() => { enable_check(e); }); + $(".possibility").keyup((e) => { + enable_check(e); + }); + $(window).scroll(() => { let self = this; if ( @@ -37,6 +43,7 @@ frappe.ready(() => { }); $("#next").click((e) => { + add_to_local_storage(); mark_active_question(e); }); @@ -224,29 +231,56 @@ const check_answer = (e = undefined) => { } else { $("#next").removeClass("hide"); } - let [answer, is_correct] = parse_options(); - add_to_local_storage(current_index, answer, is_correct); + parse_options(); }; const parse_options = () => { - let answer = []; - let is_correct = []; + let type = $(".active-question").data("type"); - $(".active-question input").each((i, element) => { - let 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"); - } + if (type == "Choices") { + $(".active-question input").each((i, element) => { + is_answer_correct(type, element); + }); + } else { + is_answer_correct(type, $(".active-question textarea")); + } +}; + +const is_answer_correct = (type, element) => { + let answer = type == "Choices" ? decodeURIComponent($(element).val()) : ""; + + frappe.call({ + async: false, + method: "lms.lms.doctype.lms_quiz.lms_quiz.check_answer", + args: { + question: $(".active-question").data("name"), + type: type, + answer: answer, + }, + callback: (data) => { + type == "Choices" + ? parse_choices(element, data.message) + : parse_possible_answers(e); + }, }); +}; - return [answer, is_correct]; +const parse_choices = (element, correct) => { + if ($(element).prop("checked")) { + self.answer.push(decodeURIComponent($(element).val())); + correct && self.is_correct.push(1); + correct ? add_icon(element, "check") : add_icon(element, "wrong"); + } else { + correct && self.is_correct.push(0); + correct + ? add_icon(element, "minus-circle-green") + : add_icon(element, "minus-circle"); + } +}; + +const parse_possible_answers = () => { + self.answer.push(decodeURIComponent($(element).val())); + correct ? self.is_correct.push(1) : self.is_correct.push(0); }; const add_icon = (element, icon) => { @@ -261,14 +295,15 @@ const add_icon = (element, icon) => { //$(element).parent().empty().html(`
${label}
`); }; -const add_to_local_storage = (current_index, answer, is_correct) => { +const add_to_local_storage = () => { + let current_index = $(".active-question").attr("data-qt-index"); let quiz_name = $("#quiz-title").data("name"); let quiz_stored = JSON.parse(localStorage.getItem(quiz_name)); let quiz_obj = { question_index: current_index, - answer: answer.join(), - is_correct: is_correct, + answer: self.answer.join(), + is_correct: self.is_correct, }; quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]); From 2fbe5dacb20d19b2b75338d2d6bc9721b20783a6 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 24 Mar 2023 18:06:42 +0530 Subject: [PATCH 2/4] feat: user input quiz portal form --- lms/lms/doctype/lms_quiz/lms_quiz.py | 16 ++----- lms/public/css/style.css | 27 +++++++++--- lms/templates/quiz.html | 8 ++-- lms/www/batch/learn.js | 31 ++++++++++--- lms/www/batch/quiz.html | 21 +++++++++ lms/www/batch/quiz.js | 65 +++++++++++++++++++--------- lms/www/batch/quiz.py | 3 +- 7 files changed, 123 insertions(+), 48 deletions(-) diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index bc158433..f9b4326a 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -148,17 +148,7 @@ def save_quiz(quiz_title, questions, quiz): } ) - question_doc.update({"question": row["question"], "multiple": row["multiple"]}) - - for num in range(1, 5): - question_doc.update( - { - "option_" + cstr(num): row["option_" + cstr(num)], - "explanation_" + cstr(num): row["explanation_" + cstr(num)], - "is_correct_" + cstr(num): row["is_correct_" + cstr(num)], - } - ) - + question_doc.update(row) question_doc.save(ignore_permissions=True) return doc.name @@ -197,6 +187,8 @@ def check_input_answers(question, answer): "LMS Quiz Question", question, fields, as_dict=1 ) for num in range(1, 5): - if question_details[f"possibility_{num}"] == answer: + current_possibility = question_details[f"possibility_{num}"] + if current_possibility and current_possibility.lower() == answer.lower(): return 1 + return 0 diff --git a/lms/public/css/style.css b/lms/public/css/style.css index d8e6b3aa..26f3cbd2 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -90,6 +90,7 @@ input[type=checkbox] { padding: 2rem 0 5rem; padding-top: 3rem; background-color: var(--bg-color); + font-size: var(--text-base); } .common-card-style { @@ -425,7 +426,6 @@ input[type=checkbox] { .lesson-links { display: flex; - align-items: center; padding: 0.5rem; color: var(--gray-900); font-size: var(--text-base); @@ -544,10 +544,10 @@ input[type=checkbox] { } .quiz-footer { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 5rem; + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 2rem; } .question { @@ -1366,7 +1366,7 @@ pre { .question-header { display: flex; align-items: center; - margin-bottom: 2rem; + margin-bottom: 1rem; } .question-number { @@ -2027,3 +2027,18 @@ select { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-gap: 1.5rem; } + +.answer-indicator { + border-radius: var(--border-radius-md); + padding: 0.2rem 0.5rem; + width: fit-content; + margin-top: 0.5rem; +} + +.answer-indicator.success { + background-color: var(--dark-green-50); +} + +.answer-indicator.failure { + background-color: var(--red-50); +} \ No newline at end of file diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index a21ac009..c9752953 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -104,19 +104,19 @@ {% endif %} - - + + {% else %} + {% set possible_answer = question["possibility_" + num] %} + +
+ +
+
+
{% if possible_answer %}{{possible_answer}}{% endif %}
+
+
+
+ {% endif %} {% endfor %} {% endfor %} diff --git a/lms/www/batch/quiz.js b/lms/www/batch/quiz.js index 9df71c49..e2065612 100644 --- a/lms/www/batch/quiz.js +++ b/lms/www/batch/quiz.js @@ -93,36 +93,41 @@ const get_questions = () => { let details = {}; let correct_options = 0; + let possibilities = 0; + details["question"] = $(el).find(".question").text(); details["question_name"] = $(el).find(".question").data("question") || ""; + details["type"] = $(el).find(".type").val(); Array.from({ length: 4 }, (x, i) => { let num = i + 1; - details[`option_${num}`] = $(el) - .find(`.option-${num} .option-input:first`) - .text(); - details[`explanation_${num}`] = $(el) - .find(`.option-${num} .option-input:last`) - .text(); + if (details.type == "Choices") { + details[`option_${num}`] = $(el) + .find(`.option-${num} .option-input:first`) + .text(); + details[`explanation_${num}`] = $(el) + .find(`.option-${num} .option-input:last`) + .text(); - let is_correct = $(el) - .find(`.option-${num} .option-checkbox`) - .find("input") - .prop("checked"); - if (is_correct) correct_options += 1; + let is_correct = $(el) + .find(`.option-${num} .option-checkbox`) + .find("input") + .prop("checked"); + if (is_correct) correct_options += 1; - details[`is_correct_${num}`] = is_correct; + details[`is_correct_${num}`] = is_correct; + } else { + let possible_answer = $(el) + .find(`.possibility-${num}`) + .text() + .trim(); + if (possible_answer) possibilities += 1; + details[`possibility_${num}`] = possible_answer; + } }); - - if (!details["option_1"] || !details["option_2"]) - frappe.throw(__("Each question must have at least two options.")); - - if (!correct_options) - frappe.throw( - __("Each question must have at least one correct option.") - ); + validate_mandatory(details, correct_options, possibilities); details["multiple"] = correct_options > 1 ? 1 : 0; questions.push(details); @@ -131,6 +136,26 @@ const get_questions = () => { return questions; }; +const validate_mandatory = (details, correct_options, possibilities) => { + if (details["type"] == "Choices") { + if (!details["option_1"] || !details["option_2"]) + frappe.throw(__("Each question must have at least two options.")); + + if (!correct_options) + frappe.throw( + __( + "Question with choices must have at least one correct option." + ) + ); + } else if (!possibilities) { + frappe.throw( + __( + "Question with user input must have at least one possible answer." + ) + ); + } +}; + const scroll_to_question_container = () => { $([document.documentElement, document.body]).animate( { diff --git a/lms/www/batch/quiz.py b/lms/www/batch/quiz.py index 7db15054..5d687fea 100644 --- a/lms/www/batch/quiz.py +++ b/lms/www/batch/quiz.py @@ -9,11 +9,12 @@ def get_context(context): context.quiz = frappe._dict() context.quiz.edit_mode = 1 else: - fields_arr = ["name", "question"] + fields_arr = ["name", "question", "type"] for num in range(1, 5): fields_arr.append("option_" + cstr(num)) fields_arr.append("is_correct_" + cstr(num)) fields_arr.append("explanation_" + cstr(num)) + fields_arr.append("possibility_" + cstr(num)) context.quiz = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1) context.quiz.questions = frappe.get_all( From b37f259804fb359919aeed7bf2283d3ea9999df6 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 27 Mar 2023 08:47:07 +0530 Subject: [PATCH 3/4] test: quiz with no possible answer --- lms/lms/doctype/lms_quiz/test_lms_quiz.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lms/lms/doctype/lms_quiz/test_lms_quiz.py b/lms/lms/doctype/lms_quiz/test_lms_quiz.py index f51a8cd4..ee79b89a 100644 --- a/lms/lms/doctype/lms_quiz/test_lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/test_lms_quiz.py @@ -19,7 +19,8 @@ class TestLMSQuiz(unittest.TestCase): quiz.append( "questions", { - "question": "Question multiple", + "question": "Question Multiple", + "type": "Choices", "option_1": "Option 1", "is_correct_1": 1, "option_2": "Option 2", @@ -35,12 +36,24 @@ class TestLMSQuiz(unittest.TestCase): "questions", { "question": "Question no correct option", + "type": "Choices", "option_1": "Option 1", "option_2": "Option 2", }, ) self.assertRaises(frappe.ValidationError, quiz.save) + def test_with_no_possible_answers(self): + quiz = frappe.get_doc("LMS Quiz", "test-quiz") + quiz.append( + "questions", + { + "question": "Question Possible Answers", + "type": "User Input", + }, + ) + self.assertRaises(frappe.ValidationError, quiz.save) + @classmethod def tearDownClass(cls) -> None: frappe.db.delete("LMS Quiz", "test-quiz") From a0e6462c13a75b516cc5e818bfe8720ba02a24fd Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 27 Mar 2023 12:53:14 +0530 Subject: [PATCH 4/4] fix: new question with possible answers --- lms/www/batch/quiz.html | 9 ++--- lms/www/batch/quiz.js | 75 ++++++++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/lms/www/batch/quiz.html b/lms/www/batch/quiz.html index 9eacd460..626d5530 100644 --- a/lms/www/batch/quiz.html +++ b/lms/www/batch/quiz.html @@ -37,7 +37,7 @@
{% if question.question %} {{ question.question }} {% endif %}
- {% for option in ["Choices", "User Input"] %} {% endfor %} @@ -46,11 +46,10 @@ {% for i in range(1,5) %} {% set num = frappe.utils.cstr(i) %} - {% if question.type == "Choices" %} {% set option = question["option_" + num] %} {% set explanation = question["explanation_" + num] %} -
+
- {% else %} {% set possible_answer = question["possibility_" + num] %} -
+
@@ -77,7 +75,6 @@
- {% endif %} {% endfor %}
{% endfor %} diff --git a/lms/www/batch/quiz.js b/lms/www/batch/quiz.js index e2065612..2077d90f 100644 --- a/lms/www/batch/quiz.js +++ b/lms/www/batch/quiz.js @@ -15,15 +15,25 @@ frappe.ready(() => { frappe.utils.copy_to_clipboard($(e.currentTarget).data("name")); }); + $(document).on("change", ".type", function () { + toggle_form($(this)); + }); + get_questions(); }); -const add_question = () => { - /* if ($(".new-quiz-card").length) { - scroll_to_question_container(); - return; - } */ +const toggle_form = (el) => { + let type = el.val(); + if (type === "Choices") { + el.siblings(".option-group").removeClass("hide"); + el.siblings(".possibility-group").addClass("hide"); + } else if (type === "User Input") { + el.siblings(".option-group").addClass("hide"); + el.siblings(".possibility-group").removeClass("hide"); + } +}; +const add_question = () => { let add_after = $(".quiz-card").length ? $(".quiz-card:last") : $("#quiz-title"); @@ -31,7 +41,11 @@ const add_question = () => {
-
`; + +
`; $(question_template).insertAfter(add_after); get_question_template(); $(".btn-save-question").removeClass("hide"); @@ -40,11 +54,31 @@ const add_question = () => { const get_question_template = () => { Array.from({ length: 4 }, (x, num) => { let option_template = get_option_template(num + 1); + let add_after = $(".quiz-card:last .option-group").length ? $(".quiz-card:last .option-group").last() - : $(".question:last"); + : $(".type:last"); question_template = $(option_template).insertAfter(add_after); }); + + Array.from({ length: 4 }, (x, num) => { + let possibility_template = get_possibility_template(num + 1); + let add_after = $(".quiz-card:last .possibility-group").length + ? $(".quiz-card:last .possibility-group").last() + : $(".quiz-card:last .option-group:last"); + question_template = $(possibility_template).insertAfter(add_after); + }); +}; + +const get_possibility_template = (num) => { + return `
+ +
+
+
+
+
+
`; }; const get_option_template = (num) => { @@ -95,6 +129,7 @@ const get_questions = () => { let correct_options = 0; let possibilities = 0; + details["element"] = el; details["question"] = $(el).find(".question").text(); details["question_name"] = $(el).find(".question").data("question") || ""; @@ -138,16 +173,21 @@ const get_questions = () => { const validate_mandatory = (details, correct_options, possibilities) => { if (details["type"] == "Choices") { - if (!details["option_1"] || !details["option_2"]) + if (!details["option_1"] || !details["option_2"]) { + scroll_to_element(details["element"]); frappe.throw(__("Each question must have at least two options.")); + } - if (!correct_options) + if (!correct_options) { + scroll_to_element(details["element"]); frappe.throw( __( "Question with choices must have at least one correct option." ) ); + } } else if (!possibilities) { + scroll_to_element(details["element"]); frappe.throw( __( "Question with user input must have at least one possible answer." @@ -157,11 +197,16 @@ const validate_mandatory = (details, correct_options, possibilities) => { }; const scroll_to_question_container = () => { - $([document.documentElement, document.body]).animate( - { - scrollTop: $(".new-quiz-card").offset().top, - }, - 1000 - ); + scroll_to_element(".new-quiz-card:last"); $(".new-quiz-card").find(".question").focus(); }; + +const scroll_to_element = (element) => { + if ($(element).length) + $([document.documentElement, document.body]).animate( + { + scrollTop: $(element).offset().top, + }, + 1000 + ); +};