From 1d8de792a5faebb36359ef55cca42e9a51a1c5cb Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 9 Jun 2023 18:09:54 +0530 Subject: [PATCH 1/5] feat: quiz option as small text --- .../doctype/lms_quiz_question/lms_quiz_question.json | 12 ++++++------ lms/www/batch/quiz.html | 3 ++- lms/www/batch/quiz.js | 8 +++++++- 3 files changed, 15 insertions(+), 8 deletions(-) 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 09856df7..3231fc8c 100644 --- a/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json +++ b/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json @@ -39,31 +39,31 @@ "fields": [ { "fieldname": "question", - "fieldtype": "Text", + "fieldtype": "Text Editor", "in_list_view": 1, "label": "Question", "reqd": 1 }, { "fieldname": "option_1", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Option 1", "mandatory_depends_on": "eval: doc.type == 'Choices'" }, { "fieldname": "option_2", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Option 2", "mandatory_depends_on": "eval: doc.type == 'Choices'" }, { "fieldname": "option_3", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Option 3" }, { "fieldname": "option_4", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Option 4" }, { @@ -206,7 +206,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-17 18:22:20.324536", + "modified": "2023-06-09 17:09:53.740179", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Question", diff --git a/lms/www/batch/quiz.html b/lms/www/batch/quiz.html index bdd3a9e3..f893c15a 100644 --- a/lms/www/batch/quiz.html +++ b/lms/www/batch/quiz.html @@ -120,7 +120,8 @@
- +
- {% else %} -
-
- +
-

- {{ quiz.title }} -

+
+
+ {{ quiz.title }} +
-
+
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
@@ -40,22 +36,27 @@
{% endif %} + +
+
-
+
{% for question in quiz.questions %} - {% 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") %} + {% set instruction = _("Choose all answers that apply") if question.type == "Choices" and question.multiple else _("Choose one answer") if question.type == "Choices" else _("Enter the correct answer") %}
-
-
{{ loop.index }}.
+ data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}"> +
+
+ {{ _("Question ") }}{{ loop.index }}: {{ instruction }}
- {{ frappe.utils.md_to_html(question.question) }} + {{ question.question }}
-
{{ instruction }}
{% if question.type == "Choices" %} @@ -64,8 +65,7 @@ {% if option %}
-
@@ -66,11 +79,11 @@ {{ _("Title") }}
- {{ _("Give your quiz a title") }} + {{ _("Add a title for the quiz") }}
- +
@@ -78,67 +91,39 @@ {% macro Question(question, index) %} {% set type = question.type if question.type else "Choices" %} -
-
-
-
- {{ _("Question") }} {{ index }} -
-
-
- -
+
+
+ + {{ index }}. + + {{ question.question.split("\n")[0] }}
-
-
-
- {{ _("Question Type") }} -
-
- - -
-
-
-
- - {% for i in range(1,5) %} - {% set num = frappe.utils.cstr(i) %} - - {% set option = question["option_" + num] %} - {% set explanation = question["explanation_" + num] %} - {% set possible_answer = question["possibility_" + num] %} - -
- -
- - - -
- -
-
- {{ _("Possible Answers") }} {{ num }} -
- -
-
- - - {% endfor %} -
-{% endmacro %} \ No newline at end of file +{% endmacro %} + +{% macro EmptyState() %} +
+
+
+ {{ _("You have not added any question yet") }} +
+
+ {{ _("Create and manage questions from here.") }} +
+
+ +
+
+
+{% endmacro %} + +{%- block script %} + {{ super() }} + {{ include_script('controls.bundle.js') }} +{% endblock %} \ No newline at end of file diff --git a/lms/www/batch/quiz.js b/lms/www/batch/quiz.js index 47afe84a..142f5f4b 100644 --- a/lms/www/batch/quiz.js +++ b/lms/www/batch/quiz.js @@ -1,80 +1,153 @@ frappe.ready(() => { - if ($(".question-card").length <= 1) { - add_question(); - } + /* if (!$("#quiz-form").data("name")) { + show_quiz_modal(); + } */ + + $("#quiz-title").focusout((e) => { + if ($("#quiz-title").val() != $("#quiz-title").data("title")) { + save_quiz({ quiz_title: $("#quiz-title").val() }); + } + }); + + $(".question-row").click((e) => { + edit_question(e); + }); $(".btn-add-question").click((e) => { - add_question(true); + show_question_modal(); }); - - $(".btn-save-question").click((e) => { - save_question(e); - }); - - $(".copy-quiz-id").click((e) => { - frappe.utils.copy_to_clipboard($(e.currentTarget).data("name")); - }); - - $(document).on("click", ".question-type", (e) => { - toggle_form($(e.currentTarget)); - }); - - get_questions(); }); -const toggle_form = (el) => { - if ($(el).hasClass("active")) { - let type = $(el).find("input").data("type"); - if (type == "Choices") { - $(el) - .closest(".field-parent") - .find(".options-group") - .removeClass("hide"); - $(el) - .closest(".field-parent") - .find(".answers-group") - .addClass("hide"); - } else { - $(el) - .closest(".field-parent") - .find(".options-group") - .addClass("hide"); - $(el) - .closest(".field-parent") - .find(".answers-group") - .removeClass("hide"); - } - } +const show_quiz_modal = () => { + let quiz_dialog = new frappe.ui.Dialog({ + title: __("Create Quiz"), + fields: [ + { + fieldtype: "Data", + label: __("Quiz Title"), + fieldname: "quiz_title", + reqd: 1, + }, + ], + primary_action: (values) => { + quiz_dialog.hide(); + save_quiz(values); + }, + }); + + quiz_dialog.show(); }; -const add_question = (scroll = false) => { - let template = $("#question-template").html(); - let index = $(".question-card:nth-last-child(2)").data("index") + 1 || 1; - template = update_index(template, index); +const show_question_modal = (values = {}) => { + let fields = get_question_fields(values); - $(template).insertBefore($("#question-template")); - scroll && scroll_to_question_container(); + this.question_dialog = new frappe.ui.Dialog({ + title: __("Add Question"), + fields: fields, + primary_action: (data) => { + if (values) data.name = values.name; + save_question(data); + }, + }); + + question_dialog.show(); }; -const update_index = (template, index) => { - const $template = $(template); - $template.attr("data-index", index); - $template.find(".question-label").text("Question " + index); - $template.find(".question-type input").attr("name", "type-" + index); - return $template.prop("outerHTML"); +const get_question_fields = (values = {}) => { + let dialog_fields = [ + { + fieldtype: "Text Editor", + fieldname: "question", + label: __("Question"), + reqd: 1, + default: values.question || "", + }, + { + fieldtype: "Select", + fieldname: "type", + label: __("Type"), + options: ["Choices", "User Input"], + default: values.type || "Choices", + }, + ]; + Array.from({ length: 4 }, (x, i) => { + num = i + 1; + + dialog_fields.push({ + fieldtype: "Section Break", + fieldname: `section_break_${num}`, + label: __(""), + }); + + let option = { + fieldtype: "Small Text", + fieldname: `option_${num}`, + label: __("Option ") + num, + depends_on: "eval:doc.type=='Choices'", + default: values[`option_${num}`] || "", + }; + + if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'"; + + dialog_fields.push(option); + + dialog_fields.push({ + fieldtype: "Data", + fieldname: `explanaion_${num}`, + label: __("Explanation"), + depends_on: "eval:doc.type=='Choices'", + default: values[`explanaion_${num}`] || "", + }); + + let is_correct = { + fieldtype: "Check", + fieldname: `is_correct_${num}`, + label: __("Is Correct"), + depends_on: "eval:doc.type=='Choices'", + default: values[`is_correct_${num}`] || 0, + }; + + if (num <= 2) + is_correct.mandatory_depends_on = "eval:doc.type=='Choices'"; + + dialog_fields.push(is_correct); + + possibility = { + fieldtype: "Small Text", + fieldname: `possibility_${num}`, + label: __("Possible Answer ") + num, + depends_on: "eval:doc.type=='User Input'", + default: values[`possibility_${num}`] || "", + }; + + if (num == 1) + possibility.mandatory_depends_on = "eval:doc.type=='User Input'"; + + dialog_fields.push(possibility); + }); + + return dialog_fields; }; -const save_question = (e) => { - if (!$("#quiz-title").val()) { - frappe.throw(__("Quiz Title is mandatory.")); - } +const edit_question = (e) => { + let question = $(e.currentTarget).data("question"); + frappe.call({ + method: "lms.lms.doctype.lms_quiz.lms_quiz.get_question_details", + args: { + question: question, + }, + callback: (data) => { + if (data.message) show_question_modal(data.message); + }, + }); +}; +const save_quiz = (values) => { frappe.call({ method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz", args: { - quiz_title: $("#quiz-title").val(), - questions: get_questions(), - quiz: $("#quiz-title").data("name") || "", + quiz_title: values.quiz_title, + quiz: $("#quiz-form").data("name") || "", }, callback: (data) => { window.location.href = `/quizzes/${data.message}`; @@ -82,90 +155,24 @@ const save_question = (e) => { }); }; -const get_questions = () => { - let questions = []; +const save_question = (values) => { + frappe.call({ + method: "lms.lms.doctype.lms_quiz.lms_quiz.save_question", + args: { + quiz: $("#quiz-form").data("name") || "", + values: values, + index: $("#quiz-form").data("index") + 1, + }, + callback: (data) => { + if (data.message) this.question_dialog.hide(); - $(".field-parent").each((i, el) => { - if (!$(el).find(".question").val()) return; - let details = {}; - let correct_options = 0; - let possibilities = 0; - - details["element"] = el; - details["question"] = $(el).find(".question").val(); - details["question_name"] = - $(el).find(".question").data("question") || ""; - details["type"] = $(el).find("label.active").find("input").data("type"); - - Array.from({ length: 4 }, (x, i) => { - let num = i + 1; - - if (details.type == "Choices") { - details[`option_${num}`] = $(el).find(`.option-${num}`).val(); - - details[`explanation_${num}`] = $(el) - .find(`.explanation-${num}`) - .val(); - - let is_correct = $(el).find(`.correct-${num}`).prop("checked"); - - if (is_correct) correct_options += 1; - - details[`is_correct_${num}`] = is_correct; - } else { - let possible_answer = $(el) - .find(`.possibility-${num}`) - .val() - .trim(); - if (possible_answer) possibilities += 1; - details[`possibility_${num}`] = possible_answer; - } - }); - validate_mandatory(details, correct_options, possibilities); - - details["multiple"] = correct_options > 1 ? 1 : 0; - questions.push(details); + frappe.show_alert({ + message: __("Saved"), + indicator: "green", + }); + setTimeout(() => { + window.location.reload(); + }, 1000); + }, }); - - return questions; -}; - -const validate_mandatory = (details, correct_options, possibilities) => { - if (details["type"] == "Choices") { - 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) { - 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." - ) - ); - } -}; - -const scroll_to_question_container = () => { - scroll_to_element(".question-card:nth-last-child(2)"); - $(".question-card:nth-last-child(2)").find(".question").focus(); -}; - -const scroll_to_element = (element) => { - if ($(element).length) - $([document.documentElement, document.body]).animate( - { - scrollTop: $(element).offset().top - 100, - }, - 1000 - ); }; diff --git a/lms/www/batch/quiz.py b/lms/www/batch/quiz.py index b24c60ed..2d83c588 100644 --- a/lms/www/batch/quiz.py +++ b/lms/www/batch/quiz.py @@ -19,11 +19,6 @@ def get_context(context): context.quiz = frappe._dict() else: 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( diff --git a/lms/www/batch/quiz_list.py b/lms/www/batch/quiz_list.py index ade57b69..ae8df7fc 100644 --- a/lms/www/batch/quiz_list.py +++ b/lms/www/batch/quiz_list.py @@ -1,5 +1,5 @@ import frappe -from lms.lms.utils import can_create_courses +from lms.lms.utils import can_create_courses, has_course_moderator_role from frappe import _ @@ -13,6 +13,5 @@ def get_context(context): raise frappe.PermissionError(_(message)) - context.quiz_list = frappe.get_all( - "LMS Quiz", {"owner": frappe.session.user}, ["name", "title"] - ) + filters = {} if has_course_moderator_role() else {"owner": frappe.session.user} + context.quiz_list = frappe.get_all("LMS Quiz", filters, ["name", "title"]) diff --git a/lms/www/courses/outline.js b/lms/www/courses/outline.js index eaf9fc6c..9df5fa58 100644 --- a/lms/www/courses/outline.js +++ b/lms/www/courses/outline.js @@ -31,7 +31,7 @@ const show_chapter_modal = (e) => { $.trim(parent.find(".chapter-description").text()) ); $("#chapter-modal").data("chapter", parent.data("chapter")); - $("#chapter-modal").data("idx", parent.data("idx")); + $("#ch apter-modal").data("idx", parent.data("idx")); } }; From 89206f94f0e2e3dac8374e62bd785205c95ecc02 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 13 Jun 2023 19:39:00 +0530 Subject: [PATCH 3/5] fix: show only first line in questions table --- lms/public/css/style.css | 12 ++++++++++-- lms/www/batch/learn.js | 3 ++- lms/www/batch/quiz.js | 9 ++------- lms/www/courses/outline.js | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 8489722c..7169ada5 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -162,8 +162,8 @@ textarea.field-input { .lesson-editor { border: 1px solid var(--gray-300); - border-radius: var(--border-radius-md); - padding-top: 0.5rem; + border-radius: var(--border-radius-md); + padding-top: 0.5rem; } .lesson-parent .breadcrumb { @@ -197,6 +197,14 @@ textarea.field-input { cursor: pointer; } +.question-row .ql-editor.read-mode p { + display: none; +} + +.question-row .ql-editor.read-mode p:first-child { + display: block; +} + .codex-editor path { stroke: var(--gray-800); } diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 2e9a7839..10bb1948 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -12,7 +12,7 @@ frappe.ready(() => { save_current_lesson(); $(".option").click((e) => { - enable_check(e); + if (!$("#check").hasClass("hide")) enable_check(e); }); $(".possibility").keyup((e) => { @@ -286,6 +286,7 @@ const show_indicator = (class_name, element) => { const add_icon = (element, icon) => { $(element).closest(".custom-checkbox").removeClass("active-option"); + $(element).closest(".option").addClass("hide"); let label = $(element).siblings(".option-text").text(); $(element).siblings(".option-text").html(`
diff --git a/lms/www/batch/quiz.js b/lms/www/batch/quiz.js index 225fbe87..4d53eff2 100644 --- a/lms/www/batch/quiz.js +++ b/lms/www/batch/quiz.js @@ -1,8 +1,4 @@ frappe.ready(() => { - /* if (!$("#quiz-form").data("name")) { - show_quiz_modal(); - } */ - $("#quiz-title").focusout((e) => { if ($("#quiz-title").val() != $("#quiz-title").data("title")) { save_quiz({ quiz_title: $("#quiz-title").val() }); @@ -76,13 +72,12 @@ const get_question_fields = (values = {}) => { dialog_fields.push({ fieldtype: "Section Break", fieldname: `section_break_${num}`, - label: __(""), }); let option = { fieldtype: "Small Text", fieldname: `option_${num}`, - label: __("Option ") + num, + label: __(`Option ${num}`), depends_on: "eval:doc.type=='Choices'", default: values[`option_${num}`] || "", }; @@ -115,7 +110,7 @@ const get_question_fields = (values = {}) => { possibility = { fieldtype: "Small Text", fieldname: `possibility_${num}`, - label: __("Possible Answer ") + num, + label: __(`Possible Answer ${num}`), depends_on: "eval:doc.type=='User Input'", default: values[`possibility_${num}`] || "", }; diff --git a/lms/www/courses/outline.js b/lms/www/courses/outline.js index 9df5fa58..eaf9fc6c 100644 --- a/lms/www/courses/outline.js +++ b/lms/www/courses/outline.js @@ -31,7 +31,7 @@ const show_chapter_modal = (e) => { $.trim(parent.find(".chapter-description").text()) ); $("#chapter-modal").data("chapter", parent.data("chapter")); - $("#ch apter-modal").data("idx", parent.data("idx")); + $("#chapter-modal").data("idx", parent.data("idx")); } }; From 9941e0e936198d61ad1bd96782e8528d85b376bc Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 13 Jun 2023 20:21:02 +0530 Subject: [PATCH 4/5] fix: linters and tests --- lms/install.py | 2 +- lms/patches/create_mentor_request_email_templates.py | 4 ++-- lms/www/batch/quiz.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/install.py b/lms/install.py index b75b5518..e551e456 100644 --- a/lms/install.py +++ b/lms/install.py @@ -62,7 +62,7 @@ def delete_lms_roles(): def set_default_home(): - frappe.db.set_value("Portal Settings", None, "default_portal_home", "/courses") + frappe.db.set_single_value("Portal Settings", None, "default_portal_home", "/courses") def create_course_creator_role(): diff --git a/lms/patches/create_mentor_request_email_templates.py b/lms/patches/create_mentor_request_email_templates.py index 006a86cf..98d08fce 100644 --- a/lms/patches/create_mentor_request_email_templates.py +++ b/lms/patches/create_mentor_request_email_templates.py @@ -22,7 +22,7 @@ def execute(): } ).insert(ignore_permissions=True) - frappe.db.set_value( + frappe.db.set_single_value( "LMS Settings", None, "mentor_request_creation", @@ -43,7 +43,7 @@ def execute(): } ).insert(ignore_permissions=True) - frappe.db.set_value( + frappe.db.set_single_value( "LMS Settings", None, "mentor_request_status_update", diff --git a/lms/www/batch/quiz.js b/lms/www/batch/quiz.js index 4d53eff2..9f592b74 100644 --- a/lms/www/batch/quiz.js +++ b/lms/www/batch/quiz.js @@ -77,7 +77,7 @@ const get_question_fields = (values = {}) => { let option = { fieldtype: "Small Text", fieldname: `option_${num}`, - label: __(`Option ${num}`), + label: __("Option") + ` ${num}`, depends_on: "eval:doc.type=='Choices'", default: values[`option_${num}`] || "", }; @@ -110,7 +110,7 @@ const get_question_fields = (values = {}) => { possibility = { fieldtype: "Small Text", fieldname: `possibility_${num}`, - label: __(`Possible Answer ${num}`), + label: __("Possible Answer") + ` ${num}`, depends_on: "eval:doc.type=='User Input'", default: values[`possibility_${num}`] || "", }; From 76ba5c188e858569c91aea29951b8e4317bbbe82 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 13 Jun 2023 20:40:48 +0530 Subject: [PATCH 5/5] fix: arguements for set_single_value --- lms/install.py | 2 +- lms/patches/create_mentor_request_email_templates.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lms/install.py b/lms/install.py index e551e456..8265d694 100644 --- a/lms/install.py +++ b/lms/install.py @@ -62,7 +62,7 @@ def delete_lms_roles(): def set_default_home(): - frappe.db.set_single_value("Portal Settings", None, "default_portal_home", "/courses") + frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses") def create_course_creator_role(): diff --git a/lms/patches/create_mentor_request_email_templates.py b/lms/patches/create_mentor_request_email_templates.py index 98d08fce..2d8667c2 100644 --- a/lms/patches/create_mentor_request_email_templates.py +++ b/lms/patches/create_mentor_request_email_templates.py @@ -24,7 +24,6 @@ def execute(): frappe.db.set_single_value( "LMS Settings", - None, "mentor_request_creation", _("Mentor Request Creation Template"), ) @@ -45,7 +44,6 @@ def execute(): frappe.db.set_single_value( "LMS Settings", - None, "mentor_request_status_update", _("Mentor Request Status Update Template"), )