From 1046d28092508fc9d4300729f762191369dd7676 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 11 Oct 2023 12:25:46 +0530 Subject: [PATCH 01/18] 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); From 12bec14c92fa8e943c74d9f2906b1612c18ac5de Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 16 Oct 2023 19:52:36 +0530 Subject: [PATCH 02/18] feat: quiz validations and marks --- .../doctype/lms_question/lms_question.json | 26 ++++++++-------- lms/lms/doctype/lms_quiz/lms_quiz.json | 25 ++++++++++++++- lms/lms/doctype/lms_quiz/lms_quiz.py | 21 ++++++++++--- .../lms_quiz_question/lms_quiz_question.json | 16 ++++++++-- .../lms_quiz_result/lms_quiz_result.json | 23 ++++++++++++-- lms/patches.txt | 3 +- lms/patches/v1_0/add_default_marks.py | 16 ++++++++++ lms/patches/v1_0/create_quiz_questions.py | 31 +++++++------------ lms/plugins.py | 23 +++++++++++++- lms/templates/quiz/quiz.html | 3 +- 10 files changed, 140 insertions(+), 47 deletions(-) create mode 100644 lms/patches/v1_0/add_default_marks.py diff --git a/lms/lms/doctype/lms_question/lms_question.json b/lms/lms/doctype/lms_question/lms_question.json index c91f4937..a7852390 100644 --- a/lms/lms/doctype/lms_question/lms_question.json +++ b/lms/lms/doctype/lms_question/lms_question.json @@ -32,11 +32,11 @@ "column_break_lknb", "explanation_4", "section_break_hkfe", - "possible_answer_1", - "possible_answer_3", + "possibility_1", + "possibility_3", "column_break_wpjr", - "possible_answer_2", - "possible_answer_4" + "possibility_2", + "possibility_4" ], "fields": [ { @@ -167,34 +167,34 @@ "fieldtype": "Section Break" }, { - "fieldname": "possible_answer_1", + "fieldname": "column_break_wpjr", + "fieldtype": "Column Break" + }, + { + "fieldname": "possibility_1", "fieldtype": "Small Text", "label": "Possible Answer 1", "mandatory_depends_on": "eval: doc.type == 'User Input'" }, { - "fieldname": "possible_answer_3", + "fieldname": "possibility_3", "fieldtype": "Small Text", "label": "Possible Answer 3" }, { - "fieldname": "column_break_wpjr", - "fieldtype": "Column Break" - }, - { - "fieldname": "possible_answer_2", + "fieldname": "possibility_2", "fieldtype": "Small Text", "label": "Possible Answer 2" }, { - "fieldname": "possible_answer_4", + "fieldname": "possibility_4", "fieldtype": "Small Text", "label": "Possible Answer 4" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-10 16:03:38.776125", + "modified": "2023-10-16 11:39:39.757008", "modified_by": "Administrator", "module": "LMS", "name": "LMS Question", diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json index dbbea28b..c5d97764 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.json +++ b/lms/lms/doctype/lms_quiz/lms_quiz.json @@ -12,6 +12,10 @@ "column_break_gaac", "max_attempts", "show_submission_history", + "section_break_hsiv", + "passing_percentage", + "column_break_rocd", + "total_marks", "section_break_sbjx", "questions", "section_break_3", @@ -90,11 +94,30 @@ "fieldname": "show_submission_history", "fieldtype": "Check", "label": "Show Submission History" + }, + { + "fieldname": "section_break_hsiv", + "fieldtype": "Section Break" + }, + { + "fieldname": "passing_percentage", + "fieldtype": "Int", + "label": "Passing Percentage" + }, + { + "fieldname": "column_break_rocd", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_marks", + "fieldtype": "Int", + "label": "Total Marks", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-04 15:26:24.457745", + "modified": "2023-10-16 17:21:33.932981", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz", diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index d91490f8..0f2ef9db 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -5,7 +5,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr +from frappe.utils import cstr, comma_and from lms.lms.doctype.lms_question.lms_question import validate_correct_answers from lms.lms.utils import ( generate_slug, @@ -15,6 +15,17 @@ from lms.lms.utils import ( class LMSQuiz(Document): + def validate(self): + self.validate_duplicate_questions() + + def validate_duplicate_questions(self): + questions = [row.question for row in self.questions] + rows = [i + 1 for i, x in enumerate(questions) if questions.count(x) > 1] + if len(rows): + frappe.throw( + _("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows))) + ) + def autoname(self): if not self.name: self.name = generate_slug(self.title, "LMS Quiz") @@ -44,11 +55,13 @@ def quiz_summary(quiz, results): for result in results: correct = result["is_correct"][0] - result["question"] = frappe.db.get_value( + question_name = frappe.db.get_value( "LMS Quiz Question", {"parent": quiz, "idx": result["question_index"] + 1}, ["question"], ) + result["question_name"] = question_name + result["question"] = frappe.db.get_value("LMS Question", question_name, "question") for point in result["is_correct"]: correct = correct and point @@ -184,9 +197,7 @@ def check_choice_answers(question, answers): 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 - ) + question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) for num in range(1, 5): if question_details[f"option_{num}"] in answers: 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 52cd5664..4be1f88e 100644 --- a/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json +++ b/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json @@ -5,22 +5,34 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "question" + "question", + "marks" ], "fields": [ { "fieldname": "question", "fieldtype": "Link", "in_list_view": 1, + "in_preview": 1, "label": "Question", "options": "LMS Question", "reqd": 1 + }, + { + "default": "1", + "fieldname": "marks", + "fieldtype": "Int", + "in_list_view": 1, + "in_preview": 1, + "label": "Marks", + "non_negative": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-10 15:42:51.791902", + "modified": "2023-10-16 19:51:03.893143", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Question", diff --git a/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json b/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json index 93487a38..72aeaef6 100644 --- a/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json +++ b/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json @@ -6,8 +6,11 @@ "engine": "InnoDB", "field_order": [ "question", - "answer", - "is_correct" + "section_break_fztv", + "question_name", + "is_correct", + "column_break_flus", + "answer" ], "fields": [ { @@ -31,12 +34,26 @@ "in_list_view": 1, "label": "Is Correct", "read_only": 1 + }, + { + "fieldname": "section_break_fztv", + "fieldtype": "Section Break" + }, + { + "fieldname": "question_name", + "fieldtype": "Link", + "label": "Question Name", + "options": "LMS Question" + }, + { + "fieldname": "column_break_flus", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-24 11:15:45.931119", + "modified": "2023-10-16 15:25:03.380843", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Result", diff --git a/lms/patches.txt b/lms/patches.txt index 60acee23..5a28133a 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -73,4 +73,5 @@ 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 lms.patches.v1_0.mark_confirmation_for_batch_students -lms.patches.v1_0.create_quiz_questions \ No newline at end of file +lms.patches.v1_0.create_quiz_questions +lms.patches.v1_0.add_default_marks #16-10-2023 \ No newline at end of file diff --git a/lms/patches/v1_0/add_default_marks.py b/lms/patches/v1_0/add_default_marks.py new file mode 100644 index 00000000..fb35a0f6 --- /dev/null +++ b/lms/patches/v1_0/add_default_marks.py @@ -0,0 +1,16 @@ +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, "marks", 1) + + quizzes = frappe.get_all("LMS Quiz", pluck="name") + + for quiz in quizzes: + questions_count = frappe.db.count("LMS Quiz Question", {"parent": quiz}) + frappe.db.set_value( + "LMS Quiz", quiz, {"total_marks": questions_count, "passing_percentage": 100} + ) diff --git a/lms/patches/v1_0/create_quiz_questions.py b/lms/patches/v1_0/create_quiz_questions.py index c53a7c00..12779cbd 100644 --- a/lms/patches/v1_0/create_quiz_questions.py +++ b/lms/patches/v1_0/create_quiz_questions.py @@ -3,31 +3,21 @@ import frappe def execute(): frappe.reload_doc("lms", "doctype", "lms_question") - frappe.reload_doc("lms", "doctype", "lms_quiz_question") + + fields = ["name", "question", "type", "multiple"] + for num in range(1, 5): + fields.append(f"option_{num}") + fields.append(f"is_correct_{num}") + fields.append(f"explanation_{num}") + fields.append(f"possibility_{num}") 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", - ], + fields=fields, ) for question in questions: + print(question.name) doc = frappe.new_doc("LMS Question") doc.update( { @@ -44,9 +34,10 @@ def execute(): f"option_{num}": question[f"option_{num}"], f"is_correct_{num}": question[f"is_correct_{num}"], f"explanation_{num}": question[f"explanation_{num}"], + f"possibility_{num}": question[f"possibility_{num}"], } ) doc.save() - + print(doc.name) frappe.db.set_value("LMS Quiz Question", question.name, "question", doc.name) diff --git a/lms/plugins.py b/lms/plugins.py index b0f40cd2..00569484 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -109,7 +109,28 @@ def quiz_renderer(quiz_name): ) +"" - quiz = frappe.get_doc("LMS Quiz", quiz_name) + quiz = frappe.db.get_value( + "LMS Quiz", + quiz_name, + ["name", "title", "max_attempts", "show_answers", "show_submission_history"], + as_dict=True, + ) + quiz.questions = [] + fields = ["name", "question", "type", "multiple"] + for num in range(1, 5): + fields.append(f"option_{num}") + fields.append(f"is_correct_{num}") + fields.append(f"explanation_{num}") + fields.append(f"possibility_{num}") + + questions = frappe.get_all( + "LMS Quiz Question", {"parent": quiz.name}, pluck="question", order_by="idx" + ) + + for question in questions: + details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) + quiz.questions.append(details) + no_of_attempts = frappe.db.count( "LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name} ) diff --git a/lms/templates/quiz/quiz.html b/lms/templates/quiz/quiz.html index 41e9cf7f..082f7d38 100644 --- a/lms/templates/quiz/quiz.html +++ b/lms/templates/quiz/quiz.html @@ -51,7 +51,8 @@ data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
- {{ _("Question ") }}{{ loop.index }}: {{ instruction }}
+ {{ _("Question ") }}{{ loop.index }}: {{ instruction }} +
{{ question.question }}
From 0111ff9c99c1adb2d903b2ea366056bcf9ee8859 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 17 Oct 2023 20:06:04 +0530 Subject: [PATCH 03/18] feat: quiz marks and passing percentage --- .../doctype/course_lesson/course_lesson.py | 8 ++- lms/lms/doctype/lms_quiz/lms_quiz.json | 12 ++-- lms/lms/doctype/lms_quiz/lms_quiz.py | 47 ++++++++++++---- .../lms_quiz_result/lms_quiz_result.json | 14 ++++- .../lms_quiz_submission.json | 39 +++++++++++-- .../lms_quiz_submission.py | 7 ++- lms/plugins.py | 17 +++++- lms/public/css/style.css | 13 +++-- lms/templates/quiz/quiz.html | 15 ++++- lms/templates/quiz/quiz.js | 8 ++- .../assignment_submission.py | 2 +- lms/www/batch/edit.js | 4 +- lms/www/batch/quiz.html | 13 +++-- lms/www/batch/quiz.js | 55 +++++++++++++++++++ lms/www/batch/quiz.py | 3 +- 15 files changed, 210 insertions(+), 47 deletions(-) diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index b2f2475f..d7b53fd4 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -99,8 +99,14 @@ def save_progress(lesson, course, status): quizzes = [value for name, value in macros if name == "Quiz"] for quiz in quizzes: + passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage") if not frappe.db.exists( - "LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user} + "LMS Quiz Submission", + { + "quiz": quiz, + "owner": frappe.session.user, + "percentage": [">=", passing_percentage], + }, ): return 0 diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json index c5d97764..202667f5 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.json +++ b/lms/lms/doctype/lms_quiz/lms_quiz.json @@ -47,7 +47,7 @@ "read_only": 1 }, { - "default": "1", + "default": "0", "fieldname": "max_attempts", "fieldtype": "Int", "label": "Max Attempts" @@ -102,7 +102,9 @@ { "fieldname": "passing_percentage", "fieldtype": "Int", - "label": "Passing Percentage" + "label": "Passing Percentage", + "non_negative": 1, + "reqd": 1 }, { "fieldname": "column_break_rocd", @@ -112,12 +114,14 @@ "fieldname": "total_marks", "fieldtype": "Int", "label": "Total Marks", - "read_only": 1 + "non_negative": 1, + "read_only": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-16 17:21:33.932981", + "modified": "2023-10-17 15:25:25.830927", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz", diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 0f2ef9db..fa1fe0e8 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -17,6 +17,7 @@ from lms.lms.utils import ( class LMSQuiz(Document): def validate(self): self.validate_duplicate_questions() + self.set_total_marks() def validate_duplicate_questions(self): questions = [row.question for row in self.questions] @@ -26,6 +27,13 @@ class LMSQuiz(Document): _("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows))) ) + def set_total_marks(self): + marks = 0 + for question in self.questions: + marks += question.marks + + self.total_marks = marks + def autoname(self): if not self.name: self.name = generate_slug(self.title, "LMS Quiz") @@ -55,27 +63,42 @@ def quiz_summary(quiz, results): for result in results: correct = result["is_correct"][0] - question_name = frappe.db.get_value( - "LMS Quiz Question", - {"parent": quiz, "idx": result["question_index"] + 1}, - ["question"], - ) - result["question_name"] = question_name - result["question"] = frappe.db.get_value("LMS Question", question_name, "question") - for point in result["is_correct"]: correct = correct and point result["is_correct"] = correct - score += correct + + question_details = frappe.db.get_value( + "LMS Quiz Question", + {"parent": quiz, "idx": result["question_index"] + 1}, + ["question", "marks"], + as_dict=1, + ) + + result["question_name"] = question_details.question + result["question"] = frappe.db.get_value( + "LMS Question", question_details.question, "question" + ) + marks = question_details.marks if correct else 0 + + result["marks"] = marks + score += marks + del result["question_index"] + quiz_details = frappe.db.get_value( + "LMS Quiz", quiz, ["total_marks", "passing_percentage"], as_dict=1 + ) + score_out_of = quiz_details.total_marks + percentage = (score / score_out_of) * 100 + submission = frappe.get_doc( { "doctype": "LMS Quiz Submission", "quiz": quiz, "result": results, "score": score, + "score_out_of": score_out_of, "member": frappe.session.user, } ) @@ -83,7 +106,9 @@ def quiz_summary(quiz, results): return { "score": score, + "score_out_of": score_out_of, "submission": submission.name, + "pass": percentage == quiz_details.passing_percentage, } @@ -213,9 +238,7 @@ def check_input_answers(question, answer): 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 - ) + question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) for num in range(1, 5): current_possibility = question_details[f"possibility_{num}"] if current_possibility and current_possibility.lower() == answer.lower(): diff --git a/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json b/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json index 72aeaef6..7c8fcfac 100644 --- a/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json +++ b/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json @@ -8,9 +8,10 @@ "question", "section_break_fztv", "question_name", - "is_correct", + "answer", "column_break_flus", - "answer" + "marks", + "is_correct" ], "fields": [ { @@ -48,12 +49,19 @@ { "fieldname": "column_break_flus", "fieldtype": "Column Break" + }, + { + "fieldname": "marks", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Marks", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-16 15:25:03.380843", + "modified": "2023-10-17 11:55:25.641214", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Result", diff --git a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json index 80ca9ffd..735d87a0 100644 --- a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json +++ b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json @@ -6,11 +6,15 @@ "engine": "InnoDB", "field_order": [ "quiz", - "score", "course", "column_break_3", "member", "member_name", + "section_break_dkpn", + "score", + "score_out_of", + "column_break_gkip", + "percentage", "section_break_6", "result" ], @@ -31,9 +35,11 @@ }, { "fieldname": "score", - "fieldtype": "Data", + "fieldtype": "Int", "in_list_view": 1, - "label": "Score" + "label": "Score", + "read_only": 1, + "reqd": 1 }, { "fieldname": "member", @@ -65,12 +71,37 @@ "label": "Course", "options": "LMS Course", "read_only": 1 + }, + { + "fetch_from": "quiz.total_marks", + "fieldname": "score_out_of", + "fieldtype": "Int", + "label": "Score Out Of", + "non_negative": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_dkpn", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_gkip", + "fieldtype": "Column Break" + }, + { + "fieldname": "percentage", + "fieldtype": "Int", + "label": "Percentage", + "non_negative": 1, + "read_only": 1, + "reqd": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-15 15:27:07.770945", + "modified": "2023-10-17 13:07:27.979974", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Submission", diff --git a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.py b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.py index d8eeba65..e481d57e 100644 --- a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.py +++ b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.py @@ -6,4 +6,9 @@ from frappe.model.document import Document class LMSQuizSubmission(Document): - pass + def before_insert(self): + self.set_percentage() + + def set_percentage(self): + if self.score and self.score_out_of: + self.percentage = (self.score / self.score_out_of) * 100 diff --git a/lms/plugins.py b/lms/plugins.py index 00569484..e3241244 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -112,7 +112,14 @@ def quiz_renderer(quiz_name): quiz = frappe.db.get_value( "LMS Quiz", quiz_name, - ["name", "title", "max_attempts", "show_answers", "show_submission_history"], + [ + "name", + "title", + "max_attempts", + "show_answers", + "show_submission_history", + "passing_percentage", + ], as_dict=True, ) quiz.questions = [] @@ -124,11 +131,15 @@ def quiz_renderer(quiz_name): fields.append(f"possibility_{num}") questions = frappe.get_all( - "LMS Quiz Question", {"parent": quiz.name}, pluck="question", order_by="idx" + "LMS Quiz Question", + filters={"parent": quiz.name}, + fields=["question", "marks"], + order_by="idx", ) for question in questions: - details = frappe.db.get_value("LMS Question", question, fields, as_dict=1) + details = frappe.db.get_value("LMS Question", question.question, fields, as_dict=1) + details["marks"] = question.marks quiz.questions.append(details) no_of_attempts = frappe.db.count( diff --git a/lms/public/css/style.css b/lms/public/css/style.css index c62f3de4..aa02192b 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -785,12 +785,13 @@ input[type=checkbox] { } .breadcrumb { - display: flex; - align-items: center; - font-size: var(--text-base); - line-height: 20px; - color: var(--gray-900); - padding: 0; + display: flex; + align-items: center; + font-size: var(--text-base); + line-height: 20px; + color: var(--gray-900); + padding: 0; + border-radius: 0; } .course-details-outline { diff --git a/lms/templates/quiz/quiz.html b/lms/templates/quiz/quiz.html index 082f7d38..84caaf50 100644 --- a/lms/templates/quiz/quiz.html +++ b/lms/templates/quiz/quiz.html @@ -6,6 +6,15 @@ {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} + {% if quiz.passing_percentage %} +
  • + {{ _("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.passing_percentage) }} +
  • +
  • + {{ _("Without passing the quiz you won't be able to complete the lesson.") }} +
  • + {% endif %} + {% if quiz.max_attempts %} {% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
  • @@ -18,8 +27,7 @@ {{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
  • {% endif %} - - +
    @@ -50,6 +58,9 @@
    +
    + {{ question.marks }} {{ _("Marks") }} +
    {{ _("Question ") }}{{ loop.index }}: {{ instruction }}
    diff --git a/lms/templates/quiz/quiz.js b/lms/templates/quiz/quiz.js index 1de0b05a..ed37a030 100644 --- a/lms/templates/quiz/quiz.js +++ b/lms/templates/quiz/quiz.js @@ -120,7 +120,6 @@ const enable_check = (e) => { const quiz_summary = (e = undefined) => { e && e.preventDefault(); let quiz_name = $("#quiz-title").data("name"); - let total_questions = $(".question").length; let self = this; frappe.call({ @@ -136,13 +135,16 @@ const quiz_summary = (e = undefined) => { $("#quiz-form").prepend( `
    ${__("Your score is")} ${data.message.score} - ${__("out of")} ${total_questions} + ${__("out of")} ${data.message.score_out_of}
    ` ); $("#try-again").attr("data-submission", data.message.submission); $("#try-again").removeClass("hide"); self.quiz_submitted = true; - if (this.hasOwnProperty("marked_as_complete")) { + if ( + this.hasOwnProperty("marked_as_complete") && + data.message.pass + ) { mark_progress(); } }, diff --git a/lms/www/assignment_submission/assignment_submission.py b/lms/www/assignment_submission/assignment_submission.py index 93ecb2f7..d1631a88 100644 --- a/lms/www/assignment_submission/assignment_submission.py +++ b/lms/www/assignment_submission/assignment_submission.py @@ -7,7 +7,7 @@ def get_context(context): context.no_cache = 1 if frappe.session.user == "Guest": - raise frappe.PermissionError(_("You don't have permission to access this page.")) + raise frappe.PermissionError(_("Please login to submit the assignment.")) context.is_moderator = has_course_moderator_role() submission = frappe.form_dict["submission"] diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index 65318ccc..13010356 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -429,9 +429,9 @@ class Quiz { } render_quiz(quiz) { - return ``; + `; } validate(savedData) { diff --git a/lms/www/batch/quiz.html b/lms/www/batch/quiz.html index ad302f78..6c829989 100644 --- a/lms/www/batch/quiz.html +++ b/lms/www/batch/quiz.html @@ -21,11 +21,12 @@
    {{ _("Questions") }}
    -
    +
    + @@ -109,7 +110,7 @@ {{ _("Show Answers") }} -
    - {% if quiz.name %} - - {% endif %} @@ -99,11 +86,23 @@ {{ _("Enter the maximum number of times a user can attempt this quiz") }}
    - {% set max_attempts = quiz.max_attempts if quiz.name else 1 %} + {% set max_attempts = quiz.max_attempts if quiz.name else 0 %}
    +
    +
    + {{ _("Passing Percentage") }} +
    +
    + {{ _("Minimum percentage required to pass this quiz.") }} +
    +
    + +
    +
    +
    {% set show_answers = quiz.show_answers or not quiz.name %}
    - {% if is_moderator %} -
    - -
    - {% endif %} - {% if batch_info.custom_component %}
    {{ batch_info.custom_component }} @@ -140,6 +132,15 @@ + + {% endif %} {% if batch_students | length and (is_moderator or is_student) %} @@ -192,6 +193,10 @@
    {{ AssessmentsSection(batch_info) }}
    + +
    + {{ EmailsSection() }} +
    {% endif %} {% if batch_students | length and (is_moderator or is_student or is_evaluator) %} @@ -376,6 +381,41 @@ {% endmacro %} + +{% macro EmailsSection() %} +
    + +
    +
    + {% for email in batch_emails %} +
    +
    + + + {% set member = frappe.db.get_value("User", email.sender, ["full_name", "username", "name", "user_image"], as_dict=1) %} + {{ widgets.Avatar(member=member, avatar_class="avatar-small") }} + + + {{ member.full_name }} +
    + + {{ frappe.utils.pretty_date(email.communication_date) }} + +
    +
    +
    +
    +
    + {{ email.content }} +
    +
    + {% endfor %} +
    +{% endmacro %} + + {% macro AssessmentList(assessments) %} {% if assessments | length %}
    diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index 5a1407ef..be3ea75d 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -843,6 +843,9 @@ const send_email_to_students = (students, values) => { message: __("Email sent successfully"), indicator: "green", }); + setTimeout(() => { + window.location.reload(); + }, 2000); }, }); }; diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index bb3ad307..3f45ef8d 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -71,6 +71,13 @@ def get_context(context): ) context.course_name_list = [course.course for course in context.batch_courses] context.assessments = get_assessments(batch_name) + context.batch_emails = frappe.get_all( + "Communication", + filters={"reference_doctype": "LMS Batch", "reference_name": batch_name}, + fields=["subject", "content", "recipients", "cc", "communication_date", "sender"], + order_by="communication_date desc", + ) + context.batch_students = get_class_student_details( batch_students, batch_courses, context.assessments ) From 6d70de2eb12b1b2e2cd448bdfee6a77d0817727a Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 25 Oct 2023 13:08:56 +0530 Subject: [PATCH 16/18] feat: certificate template --- .../lms_certificate/lms_certificate.js | 8 ++++++++ .../lms_certificate/lms_certificate.json | 14 +++++++++++--- .../lms_certificate/lms_certificate.py | 10 ++++++++++ lms/patches.txt | 3 ++- lms/patches/v1_0/add_certificate_template.py | 19 +++++++++++++++++++ lms/www/courses/certificate.py | 7 +++++-- 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 lms/patches/v1_0/add_certificate_template.py diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.js b/lms/lms/doctype/lms_certificate/lms_certificate.js index f68937c9..74cfeea8 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.js +++ b/lms/lms/doctype/lms_certificate/lms_certificate.js @@ -10,6 +10,14 @@ frappe.ui.form.on("LMS Certificate", { }, }; }); + + frm.set_query("template", function (doc) { + return { + filters: { + doc_type: "LMS Certificate", + }, + }; + }); }, refresh: (frm) => { if (frm.doc.name) diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.json b/lms/lms/doctype/lms_certificate/lms_certificate.json index 8033cd8f..83f4ba52 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.json +++ b/lms/lms/doctype/lms_certificate/lms_certificate.json @@ -8,11 +8,12 @@ "course", "member", "member_name", - "published", + "template", "column_break_3", "issue_date", "expiry_date", - "batch_name" + "batch_name", + "published" ], "fields": [ { @@ -67,11 +68,18 @@ "fieldname": "published", "fieldtype": "Check", "label": "Publish on Participant Page" + }, + { + "fieldname": "template", + "fieldtype": "Link", + "label": "Template", + "options": "Print Format", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-13 11:03:23.479255", + "modified": "2023-10-25 12:20:56.091979", "modified_by": "Administrator", "module": "LMS", "name": "LMS Certificate", diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.py b/lms/lms/doctype/lms_certificate/lms_certificate.py index daa400e4..66fd3339 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.py +++ b/lms/lms/doctype/lms_certificate/lms_certificate.py @@ -48,6 +48,15 @@ def create_certificate(course): if expires_after_yrs: expiry_date = add_years(nowdate(), expires_after_yrs) + default_certificate_template = frappe.db.get_value( + "Property Setter", + { + "doc_type": "LMS Certificate", + "property": "default_print_format", + }, + "value", + ) + certificate = frappe.get_doc( { "doctype": "LMS Certificate", @@ -55,6 +64,7 @@ def create_certificate(course): "course": course, "issue_date": nowdate(), "expiry_date": expiry_date, + "template": default_certificate_template, } ) certificate.save(ignore_permissions=True) diff --git a/lms/patches.txt b/lms/patches.txt index 5a28133a..a2c0dcba 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -74,4 +74,5 @@ execute:frappe.permissions.reset_perms("LMS Enrollment") lms.patches.v1_0.create_student_role lms.patches.v1_0.mark_confirmation_for_batch_students lms.patches.v1_0.create_quiz_questions -lms.patches.v1_0.add_default_marks #16-10-2023 \ No newline at end of file +lms.patches.v1_0.add_default_marks #16-10-2023 +lms.patches.v1_0.add_certificate_template #25-10-2023 \ No newline at end of file diff --git a/lms/patches/v1_0/add_certificate_template.py b/lms/patches/v1_0/add_certificate_template.py new file mode 100644 index 00000000..a38b4466 --- /dev/null +++ b/lms/patches/v1_0/add_certificate_template.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + default_certificate_template = frappe.db.get_value( + "Property Setter", + { + "doc_type": "LMS Certificate", + "property": "default_print_format", + }, + "value", + ) + + if frappe.db.exists("Print Format", default_certificate_template): + certificates = frappe.get_all("LMS Certificate", pluck="name") + for certificate in certificates: + frappe.db.set_value( + "LMS Certificate", certificate, "template", default_certificate_template + ) diff --git a/lms/www/courses/certificate.py b/lms/www/courses/certificate.py index 91e4de53..2931cedb 100644 --- a/lms/www/courses/certificate.py +++ b/lms/www/courses/certificate.py @@ -16,7 +16,7 @@ def get_context(context): context.doc = frappe.db.get_value( "LMS Certificate", certificate_name, - ["name", "member", "issue_date", "expiry_date", "course"], + ["name", "member", "issue_date", "expiry_date", "course", "template"], as_dict=True, ) @@ -31,7 +31,10 @@ def get_context(context): ) context.url = f"{get_url()}/courses/{context.course.name}/{context.doc.name}" - print_format = get_print_format() + if context.doc.template: + print_format = context.doc.template + else: + print_format = get_print_format() template = frappe.db.get_value( "Print Format", print_format, ["html", "css"], as_dict=True From a6c2378b560b59d3297eefa11734bf319de26429 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 25 Oct 2023 14:25:42 +0530 Subject: [PATCH 17/18] fix: certificate template pathc --- lms/patches.txt | 2 +- lms/patches/v1_0/add_certificate_template.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/patches.txt b/lms/patches.txt index a2c0dcba..3b2e34a9 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -75,4 +75,4 @@ lms.patches.v1_0.create_student_role lms.patches.v1_0.mark_confirmation_for_batch_students lms.patches.v1_0.create_quiz_questions lms.patches.v1_0.add_default_marks #16-10-2023 -lms.patches.v1_0.add_certificate_template #25-10-2023 \ No newline at end of file +lms.patches.v1_0.add_certificate_template #26-10-2023 \ No newline at end of file diff --git a/lms/patches/v1_0/add_certificate_template.py b/lms/patches/v1_0/add_certificate_template.py index a38b4466..b1eb7498 100644 --- a/lms/patches/v1_0/add_certificate_template.py +++ b/lms/patches/v1_0/add_certificate_template.py @@ -2,6 +2,7 @@ import frappe def execute(): + frappe.reload_doc("lms", "doctype", "lms_certificate") default_certificate_template = frappe.db.get_value( "Property Setter", { From ad3953070556b34d69d8db2fe2eff097df0a18b9 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 26 Oct 2023 11:30:26 +0530 Subject: [PATCH 18/18] fix: certificate download template --- lms/www/courses/certificate.html | 4 ++-- lms/www/courses/certificate.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lms/www/courses/certificate.html b/lms/www/courses/certificate.html index 15bea0cd..e6f47e82 100644 --- a/lms/www/courses/certificate.html +++ b/lms/www/courses/certificate.html @@ -19,9 +19,9 @@
    - {% if doc.member == frappe.session.user %} + {% if doc.member == frappe.session.user or is_moderator %}
    - + {{ _("Download") }} diff --git a/lms/www/courses/certificate.py b/lms/www/courses/certificate.py index 2931cedb..3b4839b4 100644 --- a/lms/www/courses/certificate.py +++ b/lms/www/courses/certificate.py @@ -2,6 +2,7 @@ import frappe from frappe import _ from frappe.utils.jinja import render_template from frappe.utils import get_url +from lms.lms.utils import has_course_moderator_role def get_context(context): @@ -30,12 +31,14 @@ def get_context(context): "User", context.doc.member, ["full_name", "username"], as_dict=True ) context.url = f"{get_url()}/courses/{context.course.name}/{context.doc.name}" + context.is_moderator = has_course_moderator_role() if context.doc.template: print_format = context.doc.template else: print_format = get_print_format() + context.print_format = print_format template = frappe.db.get_value( "Print Format", print_format, ["html", "css"], as_dict=True )