From 1046d28092508fc9d4300729f762191369dd7676 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 11 Oct 2023 12:25:46 +0530 Subject: [PATCH] feat: quiz refactor --- lms/hooks.py | 1 - lms/lms/doctype/lms_question/__init__.py | 0 lms/lms/doctype/lms_question/lms_question.js | 8 + .../doctype/lms_question/lms_question.json | 221 ++++++++++++++++++ lms/lms/doctype/lms_question/lms_question.py | 48 ++++ .../doctype/lms_question/test_lms_question.py | 9 + lms/lms/doctype/lms_quiz/lms_quiz.py | 79 +------ .../lms_quiz_question/lms_quiz_question.json | 195 +--------------- lms/patches.txt | 4 +- lms/patches/v1_0/create_quiz_questions.py | 52 +++++ .../mark_confirmation_for_batch_students.py | 9 + lms/www/batches/batch.js | 2 +- 12 files changed, 358 insertions(+), 270 deletions(-) create mode 100644 lms/lms/doctype/lms_question/__init__.py create mode 100644 lms/lms/doctype/lms_question/lms_question.js create mode 100644 lms/lms/doctype/lms_question/lms_question.json create mode 100644 lms/lms/doctype/lms_question/lms_question.py create mode 100644 lms/lms/doctype/lms_question/test_lms_question.py create mode 100644 lms/patches/v1_0/create_quiz_questions.py create mode 100644 lms/patches/v1_0/mark_confirmation_for_batch_students.py diff --git a/lms/hooks.py b/lms/hooks.py index 0c82242b..dbfd815f 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -98,7 +98,6 @@ override_doctype_class = { doc_events = { "Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"}, - "Course Lesson": {"on_update": "lms.lms.doctype.lms_quiz.lms_quiz.update_lesson_info"}, } # Scheduled Tasks diff --git a/lms/lms/doctype/lms_question/__init__.py b/lms/lms/doctype/lms_question/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_question/lms_question.js b/lms/lms/doctype/lms_question/lms_question.js new file mode 100644 index 00000000..74a28732 --- /dev/null +++ b/lms/lms/doctype/lms_question/lms_question.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("LMS Question", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_question/lms_question.json b/lms/lms/doctype/lms_question/lms_question.json new file mode 100644 index 00000000..c91f4937 --- /dev/null +++ b/lms/lms/doctype/lms_question/lms_question.json @@ -0,0 +1,221 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:QTS-{YYYY}-{#####}", + "creation": "2023-10-10 10:24:14.035772", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "question", + "type", + "multiple", + "section_break_ytxi", + "option_1", + "is_correct_1", + "column_break_fpvl", + "explanation_1", + "section_break_eiaa", + "option_2", + "is_correct_2", + "column_break_akwy", + "explanation_2", + "section_break_cwqv", + "option_3", + "is_correct_3", + "column_break_atpl", + "explanation_3", + "section_break_yqel", + "option_4", + "is_correct_4", + "column_break_lknb", + "explanation_4", + "section_break_hkfe", + "possible_answer_1", + "possible_answer_3", + "column_break_wpjr", + "possible_answer_2", + "possible_answer_4" + ], + "fields": [ + { + "fieldname": "question", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Question" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "Choices\nUser Input" + }, + { + "depends_on": "eval:doc.type == \"Choices\";", + "fieldname": "section_break_ytxi", + "fieldtype": "Section Break" + }, + { + "fieldname": "option_1", + "fieldtype": "Small Text", + "label": "Option 1", + "mandatory_depends_on": "eval: doc.type == 'Choices'" + }, + { + "default": "0", + "fieldname": "is_correct_1", + "fieldtype": "Check", + "label": "Is Correct" + }, + { + "fieldname": "column_break_fpvl", + "fieldtype": "Column Break" + }, + { + "fieldname": "explanation_1", + "fieldtype": "Small Text", + "label": "Explanation" + }, + { + "depends_on": "eval:doc.type == \"Choices\";", + "fieldname": "section_break_eiaa", + "fieldtype": "Section Break" + }, + { + "fieldname": "option_2", + "fieldtype": "Small Text", + "label": "Option 2", + "mandatory_depends_on": "eval: doc.type == 'Choices'" + }, + { + "default": "0", + "fieldname": "is_correct_2", + "fieldtype": "Check", + "label": "Is Correct" + }, + { + "fieldname": "column_break_akwy", + "fieldtype": "Column Break" + }, + { + "fieldname": "explanation_2", + "fieldtype": "Small Text", + "label": "Explanation " + }, + { + "depends_on": "eval: doc.type == 'Choices'", + "fieldname": "section_break_cwqv", + "fieldtype": "Section Break" + }, + { + "fieldname": "option_3", + "fieldtype": "Small Text", + "label": "Option 3" + }, + { + "default": "0", + "fieldname": "is_correct_3", + "fieldtype": "Check", + "label": "Is Correct" + }, + { + "fieldname": "column_break_atpl", + "fieldtype": "Column Break" + }, + { + "fieldname": "explanation_3", + "fieldtype": "Small Text", + "label": "Explanation" + }, + { + "depends_on": "eval: doc.type == 'Choices'", + "fieldname": "section_break_yqel", + "fieldtype": "Section Break" + }, + { + "fieldname": "option_4", + "fieldtype": "Small Text", + "label": "Option 4" + }, + { + "default": "0", + "fieldname": "is_correct_4", + "fieldtype": "Check", + "label": "Is Correct" + }, + { + "fieldname": "column_break_lknb", + "fieldtype": "Column Break" + }, + { + "fieldname": "explanation_4", + "fieldtype": "Small Text", + "label": "Explanation" + }, + { + "default": "0", + "fieldname": "multiple", + "fieldtype": "Check", + "hidden": 1, + "label": "Multiple Correct Answers" + }, + { + "depends_on": "eval: doc.type == 'User Input'", + "fieldname": "section_break_hkfe", + "fieldtype": "Section Break" + }, + { + "fieldname": "possible_answer_1", + "fieldtype": "Small Text", + "label": "Possible Answer 1", + "mandatory_depends_on": "eval: doc.type == 'User Input'" + }, + { + "fieldname": "possible_answer_3", + "fieldtype": "Small Text", + "label": "Possible Answer 3" + }, + { + "fieldname": "column_break_wpjr", + "fieldtype": "Column Break" + }, + { + "fieldname": "possible_answer_2", + "fieldtype": "Small Text", + "label": "Possible Answer 2" + }, + { + "fieldname": "possible_answer_4", + "fieldtype": "Small Text", + "label": "Possible Answer 4" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-10-10 16:03:38.776125", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Question", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "question" +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_question/lms_question.py b/lms/lms/doctype/lms_question/lms_question.py new file mode 100644 index 00000000..f2076284 --- /dev/null +++ b/lms/lms/doctype/lms_question/lms_question.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class LMSQuestion(Document): + def validate(self): + self.validate_correct_answers() + + +def validate_correct_answers(question): + if question.type == "Choices": + validate_duplicate_options(question) + validate_correct_options(question) + + +def validate_duplicate_options(question): + options = [] + + for num in range(1, 5): + if question.get(f"option_{num}"): + options.append(question.get(f"option_{num}")) + + if len(set(options)) != len(options): + frappe.throw(_("Duplicate options found for this question.")) + + +def validate_correct_options(question): + correct_options = get_correct_options(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.")) + + +def get_correct_options(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)) diff --git a/lms/lms/doctype/lms_question/test_lms_question.py b/lms/lms/doctype/lms_question/test_lms_question.py new file mode 100644 index 00000000..0832daa9 --- /dev/null +++ b/lms/lms/doctype/lms_question/test_lms_question.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSQuestion(FrappeTestCase): + pass diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 9cb5789c..facbffca 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -7,6 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cstr from lms.lms.utils import generate_slug, has_course_moderator_role, can_create_courses +from lms.lms.doctype.lms_question.lms_question import validate_correct_answers class LMSQuiz(Document): @@ -14,9 +15,6 @@ class LMSQuiz(Document): if not self.name: self.name = generate_slug(self.title, "LMS Quiz") - def validate(self): - validate_correct_answers(self.questions) - def get_last_submission_details(self): """Returns the latest submission for this user.""" user = frappe.session.user @@ -35,78 +33,6 @@ class LMSQuiz(Document): return result[0] -def get_correct_options(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 validate_correct_answers(questions): - for question in questions: - if question.type == "Choices": - validate_duplicate_options(question) - validate_correct_options(question) - else: - validate_possible_answer(question) - - -def validate_duplicate_options(question): - options = [] - - for num in range(1, 5): - if question.get(f"option_{num}"): - options.append(question.get(f"option_{num}")) - - if len(set(options)) != len(options): - frappe.throw( - _("Duplicate options found for this question: {0}").format( - frappe.bold(question.question) - ) - ) - - -def validate_correct_options(question): - correct_options = get_correct_options(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(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 update_lesson_info(doc, method): - if doc.quiz_id: - frappe.db.set_value( - "LMS Quiz", doc.quiz_id, {"lesson": doc.name, "course": doc.course} - ) - - @frappe.whitelist() def quiz_summary(quiz, results): score = 0 @@ -171,7 +97,8 @@ def save_quiz( @frappe.whitelist() def save_question(quiz, values, index): values = frappe._dict(json.loads(values)) - validate_correct_answers([values]) + for value in values: + validate_correct_answers(value) if values.get("name"): doc = frappe.get_doc("LMS Quiz Question", values.get("name")) 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 8e815ee3..52cd5664 100644 --- a/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json +++ b/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json @@ -5,209 +5,22 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "question", - "type", - "options_section", - "option_1", - "is_correct_1", - "column_break_5", - "explanation_1", - "section_break_5", - "option_2", - "is_correct_2", - "column_break_10", - "explanation_2", - "column_break_4", - "option_3", - "is_correct_3", - "column_break_15", - "explanation_3", - "section_break_11", - "option_4", - "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" + "question" ], "fields": [ { "fieldname": "question", - "fieldtype": "Text Editor", + "fieldtype": "Link", "in_list_view": 1, "label": "Question", + "options": "LMS Question", "reqd": 1 - }, - { - "fieldname": "option_1", - "fieldtype": "Small Text", - "label": "Option 1", - "mandatory_depends_on": "eval: doc.type == 'Choices'" - }, - { - "fieldname": "option_2", - "fieldtype": "Small Text", - "label": "Option 2", - "mandatory_depends_on": "eval: doc.type == 'Choices'" - }, - { - "fieldname": "option_3", - "fieldtype": "Small Text", - "label": "Option 3" - }, - { - "fieldname": "option_4", - "fieldtype": "Small Text", - "label": "Option 4" - }, - { - "default": "0", - "depends_on": "option_1", - "fieldname": "is_correct_1", - "fieldtype": "Check", - "label": "Is Correct" - }, - { - "default": "0", - "depends_on": "option_2", - "fieldname": "is_correct_2", - "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 - }, - { - "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" - }, - { - "depends_on": "option_1", - "fieldname": "explanation_1", - "fieldtype": "Data", - "label": "Explanation" - }, - { - "depends_on": "option_2", - "fieldname": "explanation_2", - "fieldtype": "Data", - "label": "Explanation" - }, - { - "depends_on": "option_3", - "fieldname": "explanation_3", - "fieldtype": "Data", - "label": "Explanation" - }, - { - "depends_on": "option_4", - "fieldname": "explanation_4", - "fieldtype": "Data", - "label": "Explanation" - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_15", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_20", - "fieldtype": "Column Break" - }, - { - "fieldname": "type", - "fieldtype": "Select", - "in_list_view": 1, - "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": "2023-07-04 16:43:49.837134", + "modified": "2023-10-10 15:42:51.791902", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Question", diff --git a/lms/patches.txt b/lms/patches.txt index 86e4a840..60acee23 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -71,4 +71,6 @@ lms.patches.v1_0.publish_batches lms.patches.v1_0.publish_certificates lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 execute:frappe.permissions.reset_perms("LMS Enrollment") -lms.patches.v1_0.create_student_role \ No newline at end of file +lms.patches.v1_0.create_student_role +lms.patches.v1_0.mark_confirmation_for_batch_students +lms.patches.v1_0.create_quiz_questions \ No newline at end of file diff --git a/lms/patches/v1_0/create_quiz_questions.py b/lms/patches/v1_0/create_quiz_questions.py new file mode 100644 index 00000000..c53a7c00 --- /dev/null +++ b/lms/patches/v1_0/create_quiz_questions.py @@ -0,0 +1,52 @@ +import frappe + + +def execute(): + frappe.reload_doc("lms", "doctype", "lms_question") + frappe.reload_doc("lms", "doctype", "lms_quiz_question") + + questions = frappe.get_all( + "LMS Quiz Question", + fields=[ + "name", + "question", + "type", + "multiple", + "option_1", + "is_correct_1", + "explanation_1", + "option_2", + "is_correct_2", + "explanation_2", + "option_3", + "is_correct_3", + "explanation_3", + "option_4", + "is_correct_4", + "explanation_4", + ], + ) + + for question in questions: + doc = frappe.new_doc("LMS Question") + doc.update( + { + "question": question.question, + "type": question.type, + "multiple": question.multiple, + } + ) + + for num in range(1, 5): + if question.get(f"option_{num}"): + doc.update( + { + f"option_{num}": question[f"option_{num}"], + f"is_correct_{num}": question[f"is_correct_{num}"], + f"explanation_{num}": question[f"explanation_{num}"], + } + ) + + doc.save() + + frappe.db.set_value("LMS Quiz Question", question.name, "question", doc.name) diff --git a/lms/patches/v1_0/mark_confirmation_for_batch_students.py b/lms/patches/v1_0/mark_confirmation_for_batch_students.py new file mode 100644 index 00000000..f73e0a90 --- /dev/null +++ b/lms/patches/v1_0/mark_confirmation_for_batch_students.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + frappe.reload_doc("lms", "doctype", "batch_student") + students = frappe.get_all("Batch Student", pluck="name") + + for student in students: + frappe.db.set_value("Batch Student", student, "confirmation_email_sent", 1) diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index 1f4cd60d..e67e74c6 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -653,7 +653,7 @@ const setup_calendar = (events) => { const options = get_calendar_options(element, calendar_id); const calendar = new Calendar(container, options); this.calendar_ = calendar; - console.log(options); + create_events(calendar, events); add_links_to_events(calendar, events); scroll_to_date(calendar, events);