From 677dc59399d2b5974a402dc589013013d69078a9 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 31 Aug 2023 11:49:51 +0530 Subject: [PATCH 001/112] feat: instructor notes --- .../doctype/course_lesson/course_lesson.json | 8 ++++- lms/lms/doctype/lms_course/lms_course.py | 2 ++ lms/lms/utils.py | 1 + lms/www/batch/edit.html | 14 ++++++++- lms/www/batch/edit.js | 29 ++++++++++++++++++- lms/www/batch/learn.html | 15 ++++++++-- lms/www/batch/learn.py | 18 ++++++++---- 7 files changed, 77 insertions(+), 10 deletions(-) diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index eaee75f2..dc224a69 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -23,6 +23,7 @@ "column_break_15", "file_type", "section_break_11", + "instructor_notes", "body", "help_section", "help" @@ -131,11 +132,16 @@ { "fieldname": "column_break_15", "fieldtype": "Column Break" + }, + { + "fieldname": "instructor_notes", + "fieldtype": "Text Editor", + "label": "Instructor Notes" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-02 12:42:16.926753", + "modified": "2023-08-31 11:11:22.034553", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 6b51022d..27384f3f 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -281,6 +281,7 @@ def save_lesson( preview, idx, lesson, + instructor_notes=None, youtube=None, quiz_id=None, question=None, @@ -296,6 +297,7 @@ def save_lesson( "chapter": chapter, "title": title, "body": body, + "instructor_notes": instructor_notes, "include_in_preview": preview, "youtube": youtube, "quiz_id": quiz_id, diff --git a/lms/lms/utils.py b/lms/lms/utils.py index f41480b5..f101e9f0 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -141,6 +141,7 @@ def get_lesson_details(chapter): "quiz_id", "question", "file_type", + "instructor_notes", ], as_dict=True, ) diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 6fa88e22..9c10c042 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -86,6 +86,19 @@ +
+
+ {{ _("Instructor Notes") }} +
+
+ {{ _("This notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }} +
+
+ {% if lesson.instructor_notes %} +
{{ lesson.instructor_notes }}
+ {% endif %} +
+
@@ -117,7 +130,6 @@ }; {% endif %} - {{ include_script('controls.bundle.js') }} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index dd0ae722..6b050957 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -1,7 +1,15 @@ frappe.ready(() => { - frappe.telemetry.capture("on_lesson_creation_page", "lms"); let self = this; this.quiz_in_lesson = []; + + frappe.telemetry.capture("on_lesson_creation_page", "lms"); + + if ($("#instructor-notes").length) { + frappe.require("controls.bundle.js", () => { + make_instructor_notes_component(); + }); + } + if ($("#current-lesson-content").length) { parse_string_to_lesson(); } @@ -149,6 +157,8 @@ const save = (lesson_content) => { preview: $("#preview").prop("checked") ? 1 : 0, idx: $("#lesson-title").data("index"), lesson: lesson ? lesson : "", + instructor_notes: + this.instructor_notes.get_values().instructor_notes, }, callback: (data) => { frappe.show_alert({ @@ -466,3 +476,20 @@ class Upload { }; } } + +const make_instructor_notes_component = () => { + this.instructor_notes = new frappe.ui.FieldGroup({ + fields: [ + { + fieldname: "instructor_notes", + fieldtype: "Text Editor", + default: $("#current-instructor-notes").html(), + }, + ], + body: $("#instructor-notes").get(0), + }); + this.instructor_notes.make(); + $("#instructor-notes .form-section:last").removeClass("empty-section"); + $("#instructor-notes .frappe-control").removeClass("hide-control"); + $("#instructor-notes .form-column").addClass("p-0"); +}; diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index bf4a8d77..8f44c010 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -149,17 +149,28 @@ {% if show_lesson %} {% if is_instructor and not lesson.include_in_preview %} -
+
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }} ×
{% endif %} + {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} +
+
+ {{ _("Instructor Notes") }} +
+
+ {{ lesson.instructor_notes }} +
+
+ {% endif %} + {{ render_html(lesson) }} {% else %} {% set course_link = "" + _('here') + "" %} -
+
{{ _("There is no preview available for this lesson. Please join the course to access it. Click {0} to enroll.").format(course_link) }} diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index 36244a2f..ddc036f1 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -2,7 +2,12 @@ import frappe from frappe import _ from frappe.utils import cstr, flt -from lms.lms.utils import get_lesson_url, has_course_moderator_role, is_instructor +from lms.lms.utils import ( + get_lesson_url, + has_course_moderator_role, + is_instructor, + has_course_evaluator_role, +) from lms.www.utils import ( get_common_context, redirect_to_lesson, @@ -37,20 +42,23 @@ def get_context(context): redirect_to_lesson(context.course, index_) context.lesson = get_current_lesson_details(lesson_number, context) - instructor = is_instructor(context.course.name) + context.instructor = is_instructor(context.course.name) + context.is_moderator = has_course_moderator_role() + context.is_evaluator = has_course_evaluator_role() context.show_lesson = ( context.membership or (context.lesson and context.lesson.include_in_preview) - or instructor - or has_course_moderator_role() + or context.instructor + or context.is_moderator + or context.is_evaluator ) if not context.lesson: context.lesson = frappe._dict() if frappe.form_dict.get("edit"): - if not instructor and not has_course_moderator_role(): + if not context.instructor and not context.is_moderator: raise frappe.PermissionError(_("You do not have permission to access this page.")) context.lesson.edit_mode = True else: From 2a0636b32bbf7daa427ff182a41cc567fe63cc26 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 31 Aug 2023 11:58:08 +0530 Subject: [PATCH 002/112] fix: field descriptions --- lms/www/batch/edit.html | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 9c10c042..0ba36106 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -66,13 +66,8 @@ {% macro CreateLesson() %}
-
-
- {{ _("Title") }} -
-
- {{ _("Something Short and Concise") }} -
+
+ {{ _("Title") }}
@@ -91,7 +86,7 @@ {{ _("Instructor Notes") }}
- {{ _("This notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }} + {{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
{% if lesson.instructor_notes %} From 833e714a1fa1948993dccf49ef8c8c04a8b89c2d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 31 Aug 2023 21:33:09 +0530 Subject: [PATCH 003/112] feat: embeds in lesson --- lms/hooks.py | 1 + lms/plugins.py | 14 ++++++++++++++ lms/public/css/style.css | 4 ++++ lms/www/batch/edit.html | 1 + lms/www/batch/edit.js | 29 +++++++++++++++++++++++++++++ 5 files changed, 49 insertions(+) diff --git a/lms/hooks.py b/lms/hooks.py index f8796e7e..e78659ab 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -295,6 +295,7 @@ lms_markdown_macro_renderers = { "YouTubeVideo": "lms.plugins.youtube_video_renderer", "Video": "lms.plugins.video_renderer", "Assignment": "lms.plugins.assignment_renderer", + "Embed": "lms.plugins.embed_renderer", } # page_renderer to manage profile pages diff --git a/lms/plugins.py b/lms/plugins.py index 69571476..a171e05e 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -155,6 +155,20 @@ def youtube_video_renderer(video_id): """ +def embed_renderer(details): + src = details.split("|||")[1] + return f""" + + """ + + def video_renderer(src): return ( f"" diff --git a/lms/public/css/style.css b/lms/public/css/style.css index c3f156a3..e5f5149f 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -2344,4 +2344,8 @@ select { grid-template-columns: 1fr 1fr; grid-gap: 0.5rem; margin-bottom: 1rem; +} + +.embed-tool__caption { + display: none; } \ No newline at end of file diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 0ba36106..7ad0b37b 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -128,5 +128,6 @@ + {% endblock %} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index 6b050957..ada34259 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -26,6 +26,22 @@ const setup_editor = () => { self.editor = new EditorJS({ holder: "lesson-content", tools: { + embed: { + class: Embed, + config: { + services: { + youtube: true, + vimeo: true, + codepen: true, + slides: { + regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/, + embedUrl: + "https://docs.google.com/presentation/d/e/<%= remote_id %>/embed", + html: "", + }, + }, + }, + }, header: { class: Header, inlineToolbar: ["bold", "italic", "link"], @@ -84,6 +100,15 @@ const parse_string_to_lesson = () => { file_url: video, }, }); + } else if (block.includes("{{ Embed")) { + let embed = block.match(/'([^']+)'/)[1]; + lesson_blocks.push({ + type: "embed", + data: { + service: embed.split("|||")[0], + embed: embed.split("|||")[1], + }, + }); } else if (block.includes("![]")) { let image = block.match(/\((.*?)\)/)[1]; lesson_blocks.push({ @@ -139,6 +164,10 @@ const parse_lesson_to_string = (data) => { "#".repeat(block.data.level) + ` ${block.data.text}\n`; } else if (block.type == "paragraph") { lesson_content += `${block.data.text}\n`; + } else if (block.type == "embed") { + lesson_content += `{{ Embed("${ + block.data.service + }|||${block.data.embed.replace(/&/g, "&")}") }}\n`; } }); save(lesson_content); From ffd9d56896be90348175c45d628e4ccd9603cb94 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 31 Aug 2023 23:29:56 +0530 Subject: [PATCH 004/112] feat: embed pdf --- .../doctype/course_lesson/course_lesson.json | 6 ++-- lms/lms/utils.py | 3 +- lms/plugins.py | 29 ++++++++++++------- lms/www/batch/edit.js | 10 +++++++ lms/www/batch/learn.html | 2 +- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index dc224a69..3fd75f37 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -23,8 +23,8 @@ "column_break_15", "file_type", "section_break_11", - "instructor_notes", "body", + "instructor_notes", "help_section", "help" ], @@ -135,13 +135,13 @@ }, { "fieldname": "instructor_notes", - "fieldtype": "Text Editor", + "fieldtype": "Text", "label": "Instructor Notes" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-31 11:11:22.034553", + "modified": "2023-08-31 21:47:06.314995", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index f101e9f0..5347c2e0 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -147,6 +147,8 @@ def get_lesson_details(chapter): ) lesson_details.number = flt(f"{chapter.idx}.{row.idx}") lesson_details.icon = get_lesson_icon(lesson_details.body) + if lesson_details.instructor_notes: + lesson_details.instructor_notes = markdown_to_html(lesson_details.instructor_notes) lessons.append(lesson_details) return lessons @@ -311,7 +313,6 @@ def render_html(lesson): if lesson.question: assignment = "{{ Assignment('" + lesson.question + "-" + lesson.file_type + "') }}" text = text + assignment - return markdown_to_html(text) diff --git a/lms/plugins.py b/lms/plugins.py index a171e05e..bc763238 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -156,9 +156,17 @@ def youtube_video_renderer(video_id): def embed_renderer(details): + type = details.split("|||")[0] src = details.split("|||")[1] + width = "100%" + height = "400" + + if type == "pdf": + width = "75%" + height = "600" + return f""" - ", }, + pdf: { + regex: /(https?:\/\/.*\.pdf)/, + embedUrl: "<%= remote_id %>", + html: "", + }, }, }, }, @@ -165,6 +170,11 @@ const parse_lesson_to_string = (data) => { } else if (block.type == "paragraph") { lesson_content += `${block.data.text}\n`; } else if (block.type == "embed") { + if (block.data.service == "pdf") { + if (!block.data.embed.startsWith(window.location.origin)) { + frappe.throw(__("Invalid PDF URL")); + } + } lesson_content += `{{ Embed("${ block.data.service }|||${block.data.embed.replace(/&/g, "&")}") }}\n`; diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 8f44c010..743a01fc 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -157,7 +157,7 @@ {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %}
-
+
{{ _("Instructor Notes") }}
From ce09f273734dccbe3628c1efd7f6c118133bcbe4 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 31 Aug 2023 23:32:46 +0530 Subject: [PATCH 005/112] fix: assignment renderer --- lms/lms/utils.py | 1 + lms/plugins.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 5347c2e0..2829f5d6 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -313,6 +313,7 @@ def render_html(lesson): if lesson.question: assignment = "{{ Assignment('" + lesson.question + "-" + lesson.file_type + "') }}" text = text + assignment + return markdown_to_html(text) diff --git a/lms/plugins.py b/lms/plugins.py index bc763238..59dcd3ab 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -183,21 +183,20 @@ def video_renderer(src): ) -def assignment_renderer(name): - - """supported_types = { - "Document": ".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "PDF": ".pdf", - "Image": ".png, .jpg, .jpeg", - "Video": "video/*", +def assignment_renderer(detail): + supported_types = { + "Document": ".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "PDF": ".pdf", + "Image": ".png, .jpg, .jpeg", + "Video": "video/*", } question = detail.split("-")[0] file_type = detail.split("-")[1] accept = supported_types[file_type] if file_type else "" return frappe.render_template( - "templates/assignment.html", - {"question": question, "accept": accept, "file_type": file_type}, - )""" + "templates/assignment.html", + {"question": question, "accept": accept, "file_type": file_type}, + ) def show_custom_signup(): From a9bd01b34e6ea30270f5314a48fa74325a2cb960 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 1 Sep 2023 23:14:58 +0530 Subject: [PATCH 006/112] fix: instructor notes --- lms/lms/utils.py | 4 +++- lms/www/batch/edit.js | 2 +- lms/www/batch/learn.html | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 2829f5d6..a3eb9566 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -148,7 +148,9 @@ def get_lesson_details(chapter): lesson_details.number = flt(f"{chapter.idx}.{row.idx}") lesson_details.icon = get_lesson_icon(lesson_details.body) if lesson_details.instructor_notes: - lesson_details.instructor_notes = markdown_to_html(lesson_details.instructor_notes) + lesson_details.instructor_notes_html = markdown_to_html( + lesson_details.instructor_notes + ) lessons.append(lesson_details) return lessons diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index d0efee8b..5664784c 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -521,7 +521,7 @@ const make_instructor_notes_component = () => { fields: [ { fieldname: "instructor_notes", - fieldtype: "Text Editor", + fieldtype: "Text", default: $("#current-instructor-notes").html(), }, ], diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 743a01fc..2896cd27 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -161,7 +161,7 @@ {{ _("Instructor Notes") }}
- {{ lesson.instructor_notes }} + {{ lesson.instructor_notes_html }}
{% endif %} From 07276f5c174f2143c9577629ddc5d414551dcb17 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 4 Sep 2023 23:18:45 +0530 Subject: [PATCH 007/112] fix: renamed class to batch for live classes --- lms/www/batches/batch.py | 46 ++++++++++++++++---------------- lms/www/batches/batch_details.js | 3 +++ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index 83fac64a..f70823cd 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -17,13 +17,13 @@ from lms.lms.utils import ( def get_context(context): context.no_cache = 1 - class_name = frappe.form_dict["batchname"] + batch_name = frappe.form_dict["batchname"] context.is_moderator = has_course_moderator_role() context.is_evaluator = has_course_evaluator_role() context.batch_info = frappe.db.get_value( "LMS Batch", - class_name, + batch_name, [ "name", "title", @@ -45,18 +45,18 @@ def get_context(context): ) context.reference_doctype = "LMS Batch" - context.reference_name = class_name + context.reference_name = batch_name batch_courses = frappe.get_all( "Batch Course", - {"parent": class_name}, + {"parent": batch_name}, ["name", "course", "title"], order_by="creation desc", ) batch_students = frappe.get_all( "Batch Student", - {"parent": class_name}, + {"parent": batch_name}, ["name", "student", "student_name", "username"], order_by="creation desc", ) @@ -67,31 +67,31 @@ def get_context(context): "LMS Course", fields=["name", "title"], limit_page_length=0 ) context.course_name_list = [course.course for course in context.batch_courses] - context.assessments = get_assessments(class_name) + context.assessments = get_assessments(batch_name) context.batch_students = get_class_student_details( batch_students, batch_courses, context.assessments ) - context.is_student = is_student(class_name) + context.is_student = is_student(batch_name) if not context.is_student and not context.is_moderator and not context.is_evaluator: raise frappe.PermissionError(_("You don't have permission to access this page.")) context.live_classes = frappe.get_all( "LMS Live Class", - {"class_name": class_name, "date": [">=", getdate()]}, + {"batch_name": batch_name, "date": [">=", getdate()]}, ["title", "description", "time", "date", "start_url", "join_url", "owner"], order_by="date", ) context.current_student = ( - get_current_student_details(batch_courses, class_name) if context.is_student else None + get_current_student_details(batch_courses, batch_name) if context.is_student else None ) - context.all_assignments = get_all_assignments(class_name) - context.all_quizzes = get_all_quizzes(class_name) - context.flow = get_scheduled_flow(class_name) + context.all_assignments = get_all_assignments(batch_name) + context.all_quizzes = get_all_quizzes(batch_name) + context.flow = get_scheduled_flow(batch_name) -def get_all_quizzes(class_name): +def get_all_quizzes(batch_name): filters = {} if has_course_moderator_role() else {"owner": frappe.session.user} all_quizzes = frappe.get_all("LMS Quiz", filters, ["name", "title"]) for quiz in all_quizzes: @@ -100,13 +100,13 @@ def get_all_quizzes(class_name): "doctype": "LMS Assessment", "assessment_type": "LMS Quiz", "assessment_name": quiz.name, - "parent": class_name, + "parent": batch_name, } ) return all_quizzes -def get_all_assignments(class_name): +def get_all_assignments(batch_name): filters = {} if has_course_moderator_role() else {"owner": frappe.session.user} all_assignments = frappe.get_all("LMS Assignment", filters, ["name", "title"]) for assignment in all_assignments: @@ -115,7 +115,7 @@ def get_all_assignments(class_name): "doctype": "LMS Assessment", "assessment_type": "LMS Assignment", "assessment_name": assignment.name, - "parent": class_name, + "parent": batch_name, } ) return all_assignments @@ -209,18 +209,18 @@ def sort_students(batch_students): return batch_students -def get_scheduled_flow(class_name): +def get_scheduled_flow(batch_name): chapters = [] lessons = frappe.get_all( "Scheduled Flow", - {"parent": class_name}, + {"parent": batch_name}, ["name", "lesson", "date", "start_time", "end_time"], order_by="idx", ) for lesson in lessons: - lesson = get_lesson_details(lesson, class_name) + lesson = get_lesson_details(lesson, batch_name) chapter_exists = [ chapter for chapter in chapters if chapter.chapter == lesson.chapter ] @@ -241,7 +241,7 @@ def get_scheduled_flow(class_name): return chapters -def get_lesson_details(lesson, class_name): +def get_lesson_details(lesson, batch_name): lesson.update( frappe.db.get_value( "Course Lesson", @@ -251,19 +251,19 @@ def get_lesson_details(lesson, class_name): ) ) lesson.index = get_lesson_index(lesson.lesson) - lesson.url = get_lesson_url(lesson.course, lesson.index) + "?class=" + class_name + lesson.url = get_lesson_url(lesson.course, lesson.index) + "?class=" + batch_name lesson.icon = get_lesson_icon(lesson.body) return lesson -def get_current_student_details(batch_courses, class_name): +def get_current_student_details(batch_courses, batch_name): student_details = frappe._dict() student_details.courses = frappe._dict() course_list = [course.course for course in batch_courses] get_course_progress(batch_courses, student_details) student_details.name = frappe.session.user - student_details.assessments = get_assessments(class_name, frappe.session.user) + student_details.assessments = get_assessments(batch_name, frappe.session.user) student_details.upcoming_evals = get_upcoming_evals(frappe.session.user, course_list) return student_details diff --git a/lms/www/batches/batch_details.js b/lms/www/batches/batch_details.js index 4774dfe2..19d718bc 100644 --- a/lms/www/batches/batch_details.js +++ b/lms/www/batches/batch_details.js @@ -48,6 +48,9 @@ const show_course_modal = (e) => { }, }); course_modal.show(); + setTimeout(() => { + $(".modal-body").css("min-height", "300px"); + }, 1000); }; const add_course = (values, course_name) => { From 04501143ecd90799a52346eb48e4cff893d2a63d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 11 Sep 2023 22:44:28 +0530 Subject: [PATCH 008/112] feat: apply_gst in batches --- .../doctype/lms_settings/lms_settings.json | 28 +++++++- lms/lms/doctype/payment_country/__init__.py | 0 .../payment_country/payment_country.js | 8 +++ .../payment_country/payment_country.json | 33 ++++++++++ .../payment_country/payment_country.py | 9 +++ .../payment_country/test_payment_country.py | 9 +++ lms/lms/utils.py | 65 +++++++++++++++++-- lms/www/billing/billing.html | 7 +- lms/www/billing/billing.js | 1 + lms/www/billing/billing.py | 50 +++++++------- 10 files changed, 175 insertions(+), 35 deletions(-) create mode 100644 lms/lms/doctype/payment_country/__init__.py create mode 100644 lms/lms/doctype/payment_country/payment_country.js create mode 100644 lms/lms/doctype/payment_country/payment_country.json create mode 100644 lms/lms/doctype/payment_country/payment_country.py create mode 100644 lms/lms/doctype/payment_country/test_payment_country.py diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index 4e1442c5..36741a4b 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -20,10 +20,13 @@ "allow_student_progress", "payment_section", "razorpay_key", - "default_currency", - "column_break_cfcv", "razorpay_secret", "apply_gst", + "column_break_cfcv", + "default_currency", + "show_usd_equivalent", + "apply_rounding", + "exception_country", "signup_settings_tab", "signup_settings_section", "terms_of_use", @@ -231,12 +234,31 @@ "fieldname": "apply_gst", "fieldtype": "Check", "label": "Apply GST for India" + }, + { + "default": "0", + "fieldname": "show_usd_equivalent", + "fieldtype": "Check", + "label": "Show USD Equivalent" + }, + { + "depends_on": "show_usd_equivalent", + "fieldname": "exception_country", + "fieldtype": "Table MultiSelect", + "label": "Maintain Original Currency", + "options": "Payment Country" + }, + { + "default": "0", + "fieldname": "apply_rounding", + "fieldtype": "Check", + "label": "Apply Rounding on Equivalent" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-29 09:54:48.030823", + "modified": "2023-09-11 21:56:39.996898", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", diff --git a/lms/lms/doctype/payment_country/__init__.py b/lms/lms/doctype/payment_country/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/payment_country/payment_country.js b/lms/lms/doctype/payment_country/payment_country.js new file mode 100644 index 00000000..3ad2f61b --- /dev/null +++ b/lms/lms/doctype/payment_country/payment_country.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Payment Country", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/payment_country/payment_country.json b/lms/lms/doctype/payment_country/payment_country.json new file mode 100644 index 00000000..cf493409 --- /dev/null +++ b/lms/lms/doctype/payment_country/payment_country.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-09-11 11:53:16.253740", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "country" + ], + "fields": [ + { + "fieldname": "country", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Country", + "options": "Country" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-09-11 12:04:56.048632", + "modified_by": "Administrator", + "module": "LMS", + "name": "Payment Country", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/payment_country/payment_country.py b/lms/lms/doctype/payment_country/payment_country.py new file mode 100644 index 00000000..9d834847 --- /dev/null +++ b/lms/lms/doctype/payment_country/payment_country.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PaymentCountry(Document): + pass diff --git a/lms/lms/doctype/payment_country/test_payment_country.py b/lms/lms/doctype/payment_country/test_payment_country.py new file mode 100644 index 00000000..994c246d --- /dev/null +++ b/lms/lms/doctype/payment_country/test_payment_country.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymentCountry(FrappeTestCase): + pass diff --git a/lms/lms/utils.py b/lms/lms/utils.py index a5bc80ce..7e8147e1 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -3,6 +3,8 @@ import string import frappe import json import razorpay +import requests +import base64 from frappe import _ from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result from frappe.desk.doctype.notification_log.notification_log import make_notification_logs @@ -830,19 +832,20 @@ def get_upcoming_evals(student, courses): @frappe.whitelist() -def get_payment_options(doctype, docname, phone): +def get_payment_options(doctype, docname, phone, country): if not frappe.db.exists(doctype, docname): frappe.throw(_("Invalid document provided.")) validate_phone_number(phone, True) details = get_details(doctype, docname) + details.amount, details.currency = check_multicurrency(details) + details.amount, details.gst_applied = apply_gst(details, country) - razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key") client = get_client() order = create_order(client, details.amount, details.currency) options = { - "key_id": razorpay_key, + "key_id": frappe.db.get_single_value("LMS Settings", "razorpay_key"), "name": frappe.db.get_single_value("Website Settings", "app_name"), "description": _("Payment for {0} course").format(details["title"]), "order_id": order["id"], @@ -857,6 +860,42 @@ def get_payment_options(doctype, docname, phone): return options +def check_multicurrency(amount, currency): + show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent") + exception_country = frappe.db.get_single_value("LMS Settings", "exception_country") + apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding") + country = frappe.db.get_value("User", frappe.session.user, "country") + + if not show_usd_equivalent: + return + + if currency == "USD": + return + + if exception_country and country in exception_country: + return + + exchange_rate = get_current_exchange_rate(currency, "USD") + amount = amount * exchange_rate + currency = "USD" + + if apply_rounding and amount % 100 != 0: + amount = amount + 100 - amount % 100 + + return amount, currency + + +def apply_gst(amount, country): + gst_applied = False + apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst") + + if apply_gst and country == "India": + gst_applied = True + amount = amount * 1.18 + + return amount, gst_applied + + def get_details(doctype, docname): if doctype == "LMS Course": details = frappe.db.get_value( @@ -896,8 +935,9 @@ def save_address(address): def get_client(): - razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key") - razorpay_secret = frappe.db.get_single_value("LMS Settings", "razorpay_secret") + settings = frappe.get_single("LMS Settings") + razorpay_key = settings.razorpay_key + razorpay_secret = settings.get_password("razorpay_secret", raise_exception=True) if not razorpay_key and not razorpay_secret: frappe.throw( @@ -946,7 +986,7 @@ def record_payment(address, response, client, doctype, docname): address = frappe._dict(json.loads(address)) address_name = save_address(address) - payment_details = get_payment_details(doctype, docname) + payment_details = get_payment_details(doctype, docname, address) payment_doc = frappe.new_doc("LMS Payment") payment_doc.update( { @@ -966,10 +1006,13 @@ def record_payment(address, response, client, doctype, docname): return payment_doc.name -def get_payment_details(doctype, docname): +def get_payment_details(doctype, docname, address): amount_field = "course_price" if doctype == "LMS Course" else "amount" amount = frappe.db.get_value(doctype, docname, amount_field) currency = frappe.db.get_value(doctype, docname, "currency") + apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst") + if apply_gst and address.country == "India": + amount = amount * 1.18 return { "amount": amount, @@ -999,3 +1042,11 @@ def add_student_to_batch(batchname, payment): ) student.save(ignore_permissions=True) return f"/batches/{batchname}" + + +def get_current_exchange_rate(source, target="USD"): + url = f"https://api.frankfurter.app/latest?from={source}&to={target}" + + response = requests.request("GET", url) + details = response.json() + return details["rates"][target] diff --git a/lms/www/billing/billing.html b/lms/www/billing/billing.html index 72643285..d9c5a956 100644 --- a/lms/www/billing/billing.html +++ b/lms/www/billing/billing.html @@ -30,7 +30,7 @@
- {% set label = "Course Name" if module == "course" else "Batch Name" %} + {% set label = "Course" if module == "course" else "Batch" %} {{ _(label) }} : {{ title }}
@@ -40,6 +40,11 @@ {{ _("Total Price: ") }} {{ frappe.utils.fmt_money(amount, 2, currency) }}
+ {% if gst_applied %} + + {{ _("18% GST included") }} + + {% endif %}
{% endmacro %} diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index 4f719057..1bb2e24f 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -104,6 +104,7 @@ const generate_payment_link = (e) => { doctype: doctype, docname: docname, phone: address.phone, + country: address.country, }, callback: (data) => { data.message.handler = (response) => { diff --git a/lms/www/billing/billing.py b/lms/www/billing/billing.py index 6cb9ec60..55f0abe3 100644 --- a/lms/www/billing/billing.py +++ b/lms/www/billing/billing.py @@ -1,23 +1,30 @@ import frappe from frappe import _ +from lms.lms.utils import check_multicurrency, apply_gst def get_context(context): module = frappe.form_dict.module docname = frappe.form_dict.modulename + doctype = "LMS Course" if module == "course" else "LMS Batch" + context.module = module + context.docname = docname + context.doctype = doctype + + validate_access(doctype, docname, module) + get_billing_details(context) + check_multicurrency(context) + apply_gst(context) + + +def validate_access(doctype, docname, module): if frappe.session.user == "Guest": raise frappe.PermissionError(_("You are not allowed to access this page.")) if module not in ["course", "batch"]: raise ValueError(_("Module is incorrect.")) - doctype = "LMS Course" if module == "course" else "LMS Batch" - context.module = module - context.docname = docname - context.doctype = doctype - context.apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst") - if not frappe.db.exists(doctype, docname): raise ValueError(_("Module Name is incorrect or does not exist.")) @@ -35,37 +42,32 @@ def get_context(context): if membership: raise frappe.PermissionError(_("You are already enrolled for this batch.")) - if doctype == "LMS Course": - course = frappe.db.get_value( + +def get_billing_details(context): + if context.doctype == "LMS Course": + details = frappe.db.get_value( "LMS Course", - docname, - ["title", "name", "paid_course", "course_price", "currency"], + context.docname, + ["title", "name", "paid_course", "course_price as amount", "currency"], as_dict=True, ) - if not course.paid_course: + if not details.paid_course: raise frappe.PermissionError(_("This course is free.")) - context.title = course.title - context.amount = course.course_price - context.currency = course.currency - else: - batch = frappe.db.get_value( + details = frappe.db.get_value( "LMS Batch", - docname, + context.docname, ["title", "name", "paid_batch", "amount", "currency"], as_dict=True, ) - if not batch.paid_batch: + if not details.paid_batch: raise frappe.PermissionError( _("To join this batch, please contact the Administrator.") ) - context.title = batch.title - context.amount = batch.amount - context.currency = batch.currency - - if context.apply_gst: - context.gst_amount = context.amount * 1.18 + context.title = details.title + context.amount = details.amount + context.currency = details.currency From f137f8e04821d34b99117f7e11fd2ec4d502b543 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 12 Sep 2023 12:13:41 +0530 Subject: [PATCH 009/112] feat: multicurrency --- lms/lms/doctype/lms_payment/lms_payment.json | 14 ++++++-- lms/lms/utils.py | 35 ++++++++++++-------- lms/www/billing/billing.py | 8 +++-- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/lms/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json index f89bc595..696d5af9 100644 --- a/lms/lms/doctype/lms_payment/lms_payment.json +++ b/lms/lms/doctype/lms_payment/lms_payment.json @@ -13,8 +13,9 @@ "billing_name", "payment_received", "payment_details_section", - "amount", "currency", + "amount", + "amount_with_gst", "column_break_yxpl", "order_id", "payment_id", @@ -39,7 +40,8 @@ "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Amount" + "label": "Amount", + "options": "currency" }, { "fieldname": "currency", @@ -107,11 +109,17 @@ "label": "Member", "options": "User", "reqd": 1 + }, + { + "depends_on": "eval:doc.currency == \"INR\";", + "fieldname": "amount_with_gst", + "fieldtype": "Currency", + "label": "Amount with GST" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-24 22:08:12.294960", + "modified": "2023-09-12 10:40:22.721371", "modified_by": "Administrator", "module": "LMS", "name": "LMS Payment", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 7e8147e1..e41fe117 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -838,8 +838,11 @@ def get_payment_options(doctype, docname, phone, country): validate_phone_number(phone, True) details = get_details(doctype, docname) - details.amount, details.currency = check_multicurrency(details) - details.amount, details.gst_applied = apply_gst(details, country) + details.amount, details.currency = check_multicurrency( + details.amount, details.currency + ) + if details.currency == "INR": + details.amount, details.gst_applied = apply_gst(details.amount, country) client = get_client() order = create_order(client, details.amount, details.currency) @@ -862,18 +865,17 @@ def get_payment_options(doctype, docname, phone, country): def check_multicurrency(amount, currency): show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent") - exception_country = frappe.db.get_single_value("LMS Settings", "exception_country") + exception_country = frappe.get_all( + "Payment Country", filters={"parent": "LMS Settings"}, pluck="country" + ) apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding") country = frappe.db.get_value("User", frappe.session.user, "country") - if not show_usd_equivalent: - return - - if currency == "USD": - return + if not show_usd_equivalent or currency == "USD": + return amount, currency if exception_country and country in exception_country: - return + return amount, currency exchange_rate = get_current_exchange_rate(currency, "USD") amount = amount * exchange_rate @@ -885,10 +887,13 @@ def check_multicurrency(amount, currency): return amount, currency -def apply_gst(amount, country): +def apply_gst(amount, country=None): gst_applied = False apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst") + if not country: + country = frappe.db.get_value("User", frappe.session.user, "country") + if apply_gst and country == "India": gst_applied = True amount = amount * 1.18 @@ -998,6 +1003,7 @@ def record_payment(address, response, client, doctype, docname): "payment_id": response["razorpay_payment_id"], "amount": payment_details["amount"], "currency": payment_details["currency"], + "amount_with_gst": payment_details["amount_with_gst"], "gstin": address.gstin, "pan": address.pan, } @@ -1010,13 +1016,16 @@ def get_payment_details(doctype, docname, address): amount_field = "course_price" if doctype == "LMS Course" else "amount" amount = frappe.db.get_value(doctype, docname, amount_field) currency = frappe.db.get_value(doctype, docname, "currency") - apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst") - if apply_gst and address.country == "India": - amount = amount * 1.18 + amount_with_gst = 0 + + amount, currency = check_multicurrency(amount, currency) + if currency == "INR" and address.country == "India": + amount_with_gst, gst_applied = apply_gst(amount, address.country) return { "amount": amount, "currency": currency, + "amount_with_gst": amount_with_gst, } diff --git a/lms/www/billing/billing.py b/lms/www/billing/billing.py index 55f0abe3..3c5558ce 100644 --- a/lms/www/billing/billing.py +++ b/lms/www/billing/billing.py @@ -14,8 +14,12 @@ def get_context(context): validate_access(doctype, docname, module) get_billing_details(context) - check_multicurrency(context) - apply_gst(context) + context.amount, context.currency = check_multicurrency( + context.amount, context.currency + ) + + if context.currency == "INR": + context.amount, context.gst_applied = apply_gst(context.amount, None) def validate_access(doctype, docname, module): From 1a07021bbfedae694bc782972825ee58d6a3ace0 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 12 Sep 2023 18:03:56 +0530 Subject: [PATCH 010/112] fix: quiz list in lesson page --- .../doctype/batch_student/batch_student.json | 2 +- lms/lms/doctype/lms_course/lms_course.py | 1 - lms/www/batch/edit.html | 9 -- lms/www/batch/edit.js | 114 ++++-------------- 4 files changed, 22 insertions(+), 104 deletions(-) diff --git a/lms/lms/doctype/batch_student/batch_student.json b/lms/lms/doctype/batch_student/batch_student.json index b2a8816a..699050df 100644 --- a/lms/lms/doctype/batch_student/batch_student.json +++ b/lms/lms/doctype/batch_student/batch_student.json @@ -57,7 +57,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-24 17:48:53.045539", + "modified": "2023-09-12 16:46:41.042810", "modified_by": "Administrator", "module": "LMS", "name": "Batch Student", diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 57c81d6b..74ccc51a 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -20,7 +20,6 @@ class LMSCourse(Document): self.image = validate_image(self.image) def validate_instructors(self): - print(self.is_new(), not self.instructors) if self.is_new() and not self.instructors: frappe.get_doc( { diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 7ad0b37b..641f5256 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -116,15 +116,6 @@ {%- block script %} {{ super() }} - {% if is_moderator %} - - {% endif %} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index d0efee8b..833649fd 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -1,6 +1,5 @@ frappe.ready(() => { let self = this; - this.quiz_in_lesson = []; frappe.telemetry.capture("on_lesson_creation_page", "lms"); @@ -15,7 +14,6 @@ frappe.ready(() => { } setup_editor(); - fetch_quiz_list(); $("#save-lesson").click((e) => { save_lesson(e); @@ -90,11 +88,10 @@ const parse_string_to_lesson = () => { }); } else if (block.includes("{{ Quiz")) { let quiz = block.match(/'([^']+)'/)[1]; - this.quiz_in_lesson.push(quiz); lesson_blocks.push({ type: "quiz", data: { - quiz: [quiz], + quiz: quiz, }, }); } else if (block.includes("{{ Video")) { @@ -156,9 +153,7 @@ const parse_lesson_to_string = (data) => { if (block.type == "youtube") { lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`; } else if (block.type == "quiz") { - block.data.quiz.forEach((quiz) => { - lesson_content += `{{ Quiz("${quiz}") }}\n`; - }); + lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`; } else if (block.type == "upload") { let url = block.data.file_url; lesson_content += block.data.is_video @@ -233,15 +228,6 @@ const validate_mandatory = (lesson_content) => { } }; -const fetch_quiz_list = () => { - frappe.call({ - method: "lms.lms.doctype.lms_quiz.lms_quiz.get_user_quizzes", - callback: (r) => { - self.quiz_list = r.message; - }, - }); -}; - const is_video = (url) => { let video_types = ["mov", "mp4", "mkv"]; let video_extension = url.split(".").pop(); @@ -339,57 +325,10 @@ class Quiz { this.data = data; } - get_fields() { - let fields = [ - { - fieldname: "start_section", - fieldtype: "Section Break", - label: __( - "To create a new quiz, click on the button below. Once you have created the new quiz you can come back to this lesson and add it from here." - ), - }, - { - fieldname: "create_quiz", - fieldtype: "Button", - label: __("Create Quiz"), - click: () => { - window.location.href = "/quizzes"; - }, - }, - { - fieldname: "quiz_information", - fieldtype: "HTML", - options: __("OR"), - }, - { - fieldname: "quiz_list_section", - fieldtype: "Section Break", - label: __("Select a exisitng quiz to add to this lesson."), - }, - ]; - let break_index = Math.ceil(self.quiz_list.length / 2) + 4; - - self.quiz_list.forEach((quiz) => { - fields.push({ - fieldname: quiz.name, - fieldtype: "Check", - label: quiz.title, - default: self.quiz_in_lesson.includes(quiz.name) ? 1 : 0, - read_only: self.quiz_in_lesson.includes(quiz.name) ? 1 : 0, - }); - }); - - fields.splice(break_index, 0, { - fieldname: "column_break", - fieldtype: "Column Break", - }); - return fields; - } - render() { this.wrapper = document.createElement("div"); if (this.data && this.data.quiz) { - $(this.wrapper).html(this.render_quiz()); + $(this.wrapper).html(this.render_quiz(this.data.quiz)); } else { this.render_quiz_dialog(); } @@ -398,16 +337,24 @@ class Quiz { render_quiz_dialog() { let me = this; - let fields = this.get_fields(); let quizdialog = new frappe.ui.Dialog({ title: __("Manage Quiz"), - fields: fields, + fields: [ + { + fieldname: "quiz", + fieldtype: "Link", + label: __("Quiz"), + options: "LMS Quiz", + only_select: 1, + }, + ], primary_action_label: __("Insert"), primary_action(values) { - me.analyze_quiz_list(values); + me.quiz = values.quiz; quizdialog.hide(); + $(me.wrapper).html(me.render_quiz(me.quiz)); }, - secondary_action_label: __("Create New Quiz"), + secondary_action_label: __("Create New"), secondary_action: () => { window.location.href = `/quizzes`; }, @@ -419,38 +366,19 @@ class Quiz { }, 1000); } - analyze_quiz_list(values) { - /* If quiz is selected and is not already in the lesson then render it.*/ - - this.quiz_to_render = []; - Object.keys(values).forEach((key) => { - if (values[key] === 1 && !self.quiz_in_lesson.includes(key)) { - self.quiz_in_lesson.push(key); - this.quiz_to_render.push(key); - } - }); - - $(this.wrapper).html(this.render_quiz()); - } - - render_quiz() { - let html = ``; - let quiz_list = this.data.quiz || this.quiz_to_render; - quiz_list.forEach((quiz) => { - html += `
- Quiz: ${quiz} -
`; - }); - return html; + render_quiz(quiz) { + return `
+ Quiz: ${quiz} +
`; } validate(savedData) { - return !savedData.quiz || !savedData.quiz.length ? false : true; + return !savedData.quiz || !savedData.quiz.trim() ? false : true; } save(block_content) { return { - quiz: this.data.quiz || this.quiz_to_render, + quiz: this.data.quiz || this.quiz, }; } } From 87e5096f5d0bca34f6098189edc7b6259e4e8902 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 13 Sep 2023 10:39:32 +0530 Subject: [PATCH 011/112] fix: course card edit and delete button position --- lms/public/css/style.css | 9 +++++++++ lms/www/batches/batch_details.html | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lms/public/css/style.css b/lms/public/css/style.css index c8105e46..e414f449 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -2343,4 +2343,13 @@ select { .embed-tool__caption { display: none; +} + +.card-buttons { + display: flex; + position: relative; + top: 10%; + left: 80%; + z-index: 10; + width: fit-content; } \ No newline at end of file diff --git a/lms/www/batches/batch_details.html b/lms/www/batches/batch_details.html index 1787a2da..5ed9fe76 100644 --- a/lms/www/batches/batch_details.html +++ b/lms/www/batches/batch_details.html @@ -186,9 +186,8 @@
{% for course in courses %}
- {{ widgets.CourseCard(course=course, read_only=False) }} {% if is_moderator %} -
+
{% endif %} + {{ widgets.CourseCard(course=course, read_only=False) }}
{% endfor %}
From c4ab91a5651b1bcc26fffaf331e5fafffbc553d2 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 13 Sep 2023 13:07:20 +0530 Subject: [PATCH 012/112] feat: certified participants page --- .../lms_certificate/lms_certificate.json | 9 ++++- lms/patches.txt | 3 +- lms/patches/v1_0/publish_certificates.py | 8 +++++ lms/www/batches/index.html | 2 +- .../certified_participants.html | 35 ++++++++++++++++--- .../certified_participants.js | 18 ++++++++++ .../certified_participants.py | 30 +++++++++++++--- 7 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 lms/patches/v1_0/publish_certificates.py create mode 100644 lms/www/certified_participants/certified_participants.js diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.json b/lms/lms/doctype/lms_certificate/lms_certificate.json index 39206779..8033cd8f 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.json +++ b/lms/lms/doctype/lms_certificate/lms_certificate.json @@ -8,6 +8,7 @@ "course", "member", "member_name", + "published", "column_break_3", "issue_date", "expiry_date", @@ -60,11 +61,17 @@ "in_standard_filter": 1, "label": "Batch", "options": "LMS Batch" + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Publish on Participant Page" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-29 15:23:08.637215", + "modified": "2023-09-13 11:03:23.479255", "modified_by": "Administrator", "module": "LMS", "name": "LMS Certificate", diff --git a/lms/patches.txt b/lms/patches.txt index b8e25bf8..19f9a649 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -67,4 +67,5 @@ lms.patches.v1_0.rename_lms_batch_doctype lms.patches.v1_0.rename_lms_batch_membership_doctype lms.patches.v1_0.rename_lms_class_to_lms_batch lms.patches.v1_0.rename_classes_in_navbar -lms.patches.v1_0.publish_batches \ No newline at end of file +lms.patches.v1_0.publish_batches +lms.patches.v1_0.publish_certificates \ No newline at end of file diff --git a/lms/patches/v1_0/publish_certificates.py b/lms/patches/v1_0/publish_certificates.py new file mode 100644 index 00000000..71e5861f --- /dev/null +++ b/lms/patches/v1_0/publish_certificates.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + certificates = frappe.get_all("LMS Certificate", pluck="name") + + for certificate in certificates: + frappe.db.set_value("LMS Certificate", certificate, "published", 1) diff --git a/lms/www/batches/index.html b/lms/www/batches/index.html index 21178371..d05bf6f5 100644 --- a/lms/www/batches/index.html +++ b/lms/www/batches/index.html @@ -174,7 +174,7 @@
{{ _("No Batches") }}
-
{{ _("Nothing to see here.") }}
+
{{ _("Please contact the Administrator for more information.") }}
{% endmacro %} diff --git a/lms/www/certified_participants/certified_participants.html b/lms/www/certified_participants/certified_participants.html index 6435afb8..988c9fac 100644 --- a/lms/www/certified_participants/certified_participants.html +++ b/lms/www/certified_participants/certified_participants.html @@ -7,11 +7,28 @@
-
+ {% if course_filter | length %} + + {% endif %} +
{{ _("Certified Participants") }}
- {{ ParticipantsList() }} + {% if participants | length %} + {{ ParticipantsList() }} + {% else %} + {{ EmptyState() }} + {% endif %} +
{% endblock %} @@ -21,15 +38,25 @@ {% for participant in participants %}
{{ widgets.Avatar(member=participant, avatar_class="avatar-large") }} -
+
{{ participant.full_name }}
{% for course in participant.courses %} -
+
{{ course }}
{% endfor %}
{% endfor %}
+{% endmacro %} + +{% macro EmptyState() %} +
+ +
+
{{ _("No Certified Participants") }}
+
{{ _("Enroll in a batch to get certified.") }}
+
+
{% endmacro %} \ No newline at end of file diff --git a/lms/www/certified_participants/certified_participants.js b/lms/www/certified_participants/certified_participants.js new file mode 100644 index 00000000..af5b1340 --- /dev/null +++ b/lms/www/certified_participants/certified_participants.js @@ -0,0 +1,18 @@ +frappe.ready(() => { + $("#certificate-filter").change((e) => { + filter_certified_participants(); + }); +}); + +const filter_certified_participants = () => { + const certificate = $("#certificate-filter").val(); + $(".common-card-style").removeClass("hide"); + + if (certificate) { + $(".common-card-style").addClass("hide"); + $(`[data-course='${certificate}']`) + .closest(".common-card-style") + .removeClass("hide"); + console.log(certificate); + } +}; diff --git a/lms/www/certified_participants/certified_participants.py b/lms/www/certified_participants/certified_participants.py index a0602219..2dc9b238 100644 --- a/lms/www/certified_participants/certified_participants.py +++ b/lms/www/certified_participants/certified_participants.py @@ -3,20 +3,40 @@ import frappe def get_context(context): context.no_cache = 1 - context.members = frappe.get_all( - "LMS Certificate", pluck="member", order_by="creation desc", distinct=1 + members = frappe.get_all( + "LMS Certificate", + filters={"published": 1}, + pluck="member", + order_by="issue_date desc", + distinct=1, ) participants = [] - for member in context.members: + course_filter = [] + for member in members: details = frappe.db.get_value( "User", member, ["name", "full_name", "user_image", "username", "enabled"], as_dict=1 ) - courses = frappe.get_all("LMS Certificate", {"member": member}, pluck="course") + courses = frappe.get_all( + "LMS Certificate", + filters={"member": member, "published": 1}, + fields=["course", "issue_date"], + ) details.courses = [] for course in courses: - details.courses.append(frappe.db.get_value("LMS Course", course, "title")) + + if not details.issue_date: + details.issue_date = course.issue_date + + title = frappe.db.get_value("LMS Course", course.course, "title") + details.courses.append(title) + + if title not in course_filter: + course_filter.append(title) + if details.enabled: participants.append(details) + participants = sorted(participants, key=lambda d: d.issue_date, reverse=True) context.participants = participants + context.course_filter = course_filter From 82b8853f39ec3742757ee65d4fc3a518e491c095 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 13 Sep 2023 15:10:52 +0530 Subject: [PATCH 013/112] fix: patches --- lms/patches/v1_0/publish_batches.py | 1 + lms/patches/v1_0/publish_certificates.py | 1 + lms/www/certified_participants/certified_participants.html | 1 + 3 files changed, 3 insertions(+) diff --git a/lms/patches/v1_0/publish_batches.py b/lms/patches/v1_0/publish_batches.py index 4ef5a8e2..7b16c4f1 100644 --- a/lms/patches/v1_0/publish_batches.py +++ b/lms/patches/v1_0/publish_batches.py @@ -2,6 +2,7 @@ import frappe def execute(): + frappe.reload_doc("lms", "doctype", "lms_batch") batches = frappe.get_all("LMS Batch", pluck="name") for batch in batches: diff --git a/lms/patches/v1_0/publish_certificates.py b/lms/patches/v1_0/publish_certificates.py index 71e5861f..608f0d54 100644 --- a/lms/patches/v1_0/publish_certificates.py +++ b/lms/patches/v1_0/publish_certificates.py @@ -2,6 +2,7 @@ import frappe def execute(): + frappe.reload_doc("lms", "doctype", "lms_certificate") certificates = frappe.get_all("LMS Certificate", pluck="name") for certificate in certificates: diff --git a/lms/www/certified_participants/certified_participants.html b/lms/www/certified_participants/certified_participants.html index 988c9fac..f4e49cb3 100644 --- a/lms/www/certified_participants/certified_participants.html +++ b/lms/www/certified_participants/certified_participants.html @@ -46,6 +46,7 @@ {{ course }}
{% endfor %} +
{% endfor %} From 29860583f4ec6ae3a5f95fe398a270b4ba4248a2 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 14 Sep 2023 12:34:34 +0530 Subject: [PATCH 014/112] fix: batch course sequence id issue --- lms/lms/doctype/lms_batch/lms_batch.py | 4 ++++ lms/patches.txt | 3 ++- lms/patches/v1_0/change_naming_for_batch_course.py | 6 ++++++ lms/www/batches/batch_details.js | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 lms/patches/v1_0/change_naming_for_batch_course.py diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index 3c6e034f..cff87d28 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -245,6 +245,10 @@ def fetch_lessons(courses): @frappe.whitelist() def add_course(course, parent, name=None, evaluator=None): frappe.only_for("Moderator") + + if frappe.db.exists("Batch Course", {"course": course, "parent": parent}): + frappe.throw(_("Course already added to the batch.")) + if name: doc = frappe.get_doc("Batch Course", name) else: diff --git a/lms/patches.txt b/lms/patches.txt index 19f9a649..ef457315 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -68,4 +68,5 @@ lms.patches.v1_0.rename_lms_batch_membership_doctype lms.patches.v1_0.rename_lms_class_to_lms_batch lms.patches.v1_0.rename_classes_in_navbar lms.patches.v1_0.publish_batches -lms.patches.v1_0.publish_certificates \ No newline at end of file +lms.patches.v1_0.publish_certificates +lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 \ No newline at end of file diff --git a/lms/patches/v1_0/change_naming_for_batch_course.py b/lms/patches/v1_0/change_naming_for_batch_course.py new file mode 100644 index 00000000..7a93720c --- /dev/null +++ b/lms/patches/v1_0/change_naming_for_batch_course.py @@ -0,0 +1,6 @@ +import frappe + + +def execute(): + frappe.db.create_sequence("Batch Course", check_not_exists=True) + frappe.db.set_next_sequence_val("Batch Course", 500, is_val_used=False) diff --git a/lms/www/batches/batch_details.js b/lms/www/batches/batch_details.js index 19d718bc..92070ece 100644 --- a/lms/www/batches/batch_details.js +++ b/lms/www/batches/batch_details.js @@ -31,6 +31,7 @@ const show_course_modal = (e) => { reqd: 1, only_select: 1, default: course || "", + read_only: course ? 1 : 0, }, { fieldtype: "Link", From 24e9f46e2fc2a24abd0e035eb4160b3cc9abf91c Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 15 Sep 2023 21:55:06 +0530 Subject: [PATCH 015/112] feat: batch timetable --- lms/lms/doctype/lms_batch/lms_batch.js | 17 ++- lms/lms/doctype/lms_batch/lms_batch.json | 18 +-- lms/lms/doctype/lms_batch/lms_batch.py | 51 ++++++- .../doctype/lms_batch_timetable/__init__.py | 0 .../lms_batch_timetable.js | 8 + .../lms_batch_timetable.json | 81 +++++++++++ .../lms_batch_timetable.py | 9 ++ .../test_lms_batch_timetable.py | 9 ++ .../lms_live_class/lms_live_class.json | 4 +- lms/public/css/style.css | 65 +++++++++ lms/www/batches/batch.html | 123 ++++++---------- lms/www/batches/batch.js | 137 ++++++++++++++++++ lms/www/batches/batch.py | 49 ++----- lms/www/batches/index.py | 1 - lms/www/utils.py | 4 + 15 files changed, 448 insertions(+), 128 deletions(-) create mode 100644 lms/lms/doctype/lms_batch_timetable/__init__.py create mode 100644 lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.js create mode 100644 lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json create mode 100644 lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.py create mode 100644 lms/lms/doctype/lms_batch_timetable/test_lms_batch_timetable.py diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index 45f9d0b5..a4277bd2 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -10,10 +10,23 @@ frappe.ui.form.on("LMS Batch", { }, }; }); + + frm.set_query("reference_doctype", "timetable", function () { + let doctypes = [ + "Course Lesson", + "LMS Quiz", + "LMS Assignment", + "LMS Live Class", + ]; + return { + filters: { + name: ["in", doctypes], + }, + }; + }); }, fetch_lessons: (frm) => { - frm.clear_table("scheduled_flow"); frappe.call({ method: "lms.lms.doctype.lms_batch.lms_batch.fetch_lessons", args: { @@ -22,7 +35,7 @@ frappe.ui.form.on("LMS Batch", { callback: (r) => { if (r.message) { r.message.forEach((lesson) => { - let row = frm.add_child("scheduled_flow"); + let row = frm.add_child("timetable"); row.lesson = lesson.name; row.lesson_title = lesson.title; }); diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index a7949ce3..958cbb47 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -36,7 +36,7 @@ "assessment", "schedule_tab", "fetch_lessons", - "scheduled_flow" + "timetable" ], "fields": [ { @@ -146,12 +146,6 @@ "fieldtype": "Autocomplete", "label": "Category" }, - { - "fieldname": "scheduled_flow", - "fieldtype": "Table", - "label": "Scheduled Flow", - "options": "Scheduled Flow" - }, { "fieldname": "section_break_ubxi", "fieldtype": "Section Break" @@ -164,7 +158,7 @@ { "fieldname": "schedule_tab", "fieldtype": "Tab Break", - "label": "Schedule" + "label": "Timetable" }, { "fieldname": "section_break_gsac", @@ -199,11 +193,17 @@ "fieldname": "published", "fieldtype": "Check", "label": "Published" + }, + { + "fieldname": "timetable", + "fieldtype": "Table", + "label": "Timetable", + "options": "LMS Batch Timetable" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-12 12:30:06.565104", + "modified": "2023-09-14 12:51:11.847853", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch", diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index 3c6e034f..10664dd6 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -8,7 +8,8 @@ import json from frappe import _ from frappe.model.document import Document from frappe.utils import cint, format_date, format_datetime -from lms.lms.utils import get_lessons +from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url +from lms.www.utils import get_quiz_details, get_assignment_details class LMSBatch(Document): @@ -19,7 +20,7 @@ class LMSBatch(Document): self.validate_duplicate_students() self.validate_duplicate_assessments() self.validate_membership() - self.validate_schedule() + self.validate_timetable() def validate_duplicate_students(self): students = [row.student for row in self.students] @@ -68,8 +69,8 @@ class LMSBatch(Document): if cint(self.seat_count) < len(self.students): frappe.throw(_("There are no seats available in this batch.")) - def validate_schedule(self): - for schedule in self.scheduled_flow: + def validate_timetable(self): + for schedule in self.timetable: if schedule.start_time and schedule.end_time: if ( schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time @@ -262,3 +263,45 @@ def add_course(course, parent, name=None, evaluator=None): doc.save() return doc.name + + +@frappe.whitelist() +def get_batch_timetable(batch): + timetable = frappe.get_all( + "LMS Batch Timetable", + filters={"parent": batch}, + fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"], + order_by="date", + ) + + for entry in timetable: + entry.title = frappe.db.get_value( + entry.reference_doctype, entry.reference_docname, "title" + ) + assessment = frappe._dict({"assessment_name": entry.reference_docname}) + + if entry.reference_doctype == "Course Lesson": + entry.icon = "icon-list" + course = frappe.db.get_value( + entry.reference_doctype, entry.reference_docname, "course" + ) + entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname)) + + elif entry.reference_doctype == "LMS Quiz": + entry.icon = "icon-quiz" + entry.url = "/quizzes" + details = get_quiz_details(assessment, frappe.session.user) + entry.update(details) + + elif entry.reference_doctype == "LMS Assignment": + entry.icon = "icon-quiz" + details = get_assignment_details(assessment, frappe.session.user) + entry.update(details) + + elif entry.reference_doctype == "LMS Live Class": + entry.icon = "icon-call" + entry.url = frappe.db.get_value( + entry.reference_doctype, entry.reference_docname, "join_url" + ) + + return timetable diff --git a/lms/lms/doctype/lms_batch_timetable/__init__.py b/lms/lms/doctype/lms_batch_timetable/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.js b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.js new file mode 100644 index 00000000..4cecd9c3 --- /dev/null +++ b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("LMS Batch Timetable", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json new file mode 100644 index 00000000..1eacc738 --- /dev/null +++ b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2023-09-14 12:44:51.098956", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "column_break_htdc", + "reference_doctype", + "reference_docname", + "date", + "column_break_merq", + "start_time", + "end_time", + "duration" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference DocType", + "options": "DocType" + }, + { + "fieldname": "reference_docname", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference DocName", + "options": "reference_doctype" + }, + { + "fieldname": "column_break_merq", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date" + }, + { + "fieldname": "start_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "Start Time" + }, + { + "fieldname": "duration", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Duration" + }, + { + "fieldname": "column_break_htdc", + "fieldtype": "Column Break" + }, + { + "fieldname": "end_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "End Time" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-09-15 10:35:40.642660", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Batch Timetable", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.py b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.py new file mode 100644 index 00000000..257896a6 --- /dev/null +++ b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LMSBatchTimetable(Document): + pass diff --git a/lms/lms/doctype/lms_batch_timetable/test_lms_batch_timetable.py b/lms/lms/doctype/lms_batch_timetable/test_lms_batch_timetable.py new file mode 100644 index 00000000..68754a5f --- /dev/null +++ b/lms/lms/doctype/lms_batch_timetable/test_lms_batch_timetable.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSBatchTimetable(FrappeTestCase): + pass diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.json b/lms/lms/doctype/lms_live_class/lms_live_class.json index 1e68dbfc..af87c823 100644 --- a/lms/lms/doctype/lms_live_class/lms_live_class.json +++ b/lms/lms/doctype/lms_live_class/lms_live_class.json @@ -126,7 +126,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-03-14 18:44:48.813103", + "modified": "2023-09-14 15:02:44.081474", "modified_by": "Administrator", "module": "LMS", "name": "LMS Live Class", @@ -157,8 +157,10 @@ "write": 1 } ], + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], + "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/lms/public/css/style.css b/lms/public/css/style.css index e414f449..ada6eb38 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -2352,4 +2352,69 @@ select { left: 80%; z-index: 10; width: fit-content; +} + +.toastui-calendar-milestone { + display: none; +} + +.toastui-calendar-task { + display: none; +} + +.toastui-calendar-panel-resizer { + display: none; +} + +.toastui-calendar-day-name__date { + font-size: var(--text-base) !important; +} + +.toastui-calendar-day-name__name { + font-size: var(--text-base) !important; +} + +.toastui-calendar-day-view-day-names, .toastui-calendar-week-view-day-names { + border-bottom: none !important; +} + +.toastui-calendar-layout { + border: 1px solid var(--gray-200) !important; + border-radius: var(--border-radius-md) !important; + background-color: var(--gray-100) !important; +} + +.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week { + border-top: none !important; +} + +.toastui-calendar-panel.toastui-calendar-time { + height: 80% !important; +} + +.toastui-calendar-panel.toastui-calendar-week-view-day-names { + background-color: var(--gray-50) !important; +} + +.toastui-calendar-allday { + border-bottom: 1px solid var(--gray-200) !important; +} + +.calendar-navigation { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 1rem; +} + +.calendar-range { + margin: 0 2rem; + font-weight: 500; + color: var(--text-color); +} + +.calendar-event-title { + font-size: var(--text-base); + font-weight: 500; + margin-top: 0.25rem; } \ No newline at end of file diff --git a/lms/www/batches/batch.html b/lms/www/batches/batch.html index b6398a26..2cf378d0 100644 --- a/lms/www/batches/batch.html +++ b/lms/www/batches/batch.html @@ -88,7 +88,7 @@
- {% if flow | length %} -
- {{ ScheduleSection(flow) }} + {% if show_timetable %} +
+ {{ Timetable() }}
{% endif %} @@ -513,79 +510,49 @@ {% endmacro %} -{% macro ScheduleSection(flow) %} +{% macro Timetable() %}
- {{ _("Schedule") }} + {{ _("Timetable") }}
- -
- {% for chapter in flow %} -
-
- -
- {{ chapter.chapter_title }} -
-
- -
- {% endfor %} +
+ + +
+
+
+ +
{% endmacro %} @@ -595,4 +562,6 @@ frappe.boot.single_types = [] let courses = {{ course_list | json }}; + + {% endblock %} \ No newline at end of file diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index 1fdefaa3..f8cfbc8a 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -2,6 +2,22 @@ frappe.ready(() => { let self = this; frappe.require("controls.bundle.js"); + if ($("#calendar").length) { + setup_timetable(); + } + + if ($("#calendar").length) { + $(document).on("click", "#prev-week", (e) => { + this.calendar_ && this.calendar_.prev(); + }); + } + + if ($("#calendar").length) { + $(document).on("click", "#next-week", (e) => { + this.calendar_ && this.calendar_.next(); + }); + } + if ($("#live-class-form").length) { setTimeout(() => { make_live_class_form(); @@ -606,3 +622,124 @@ const submit_evaluation_form = (values) => { }, }); }; + +const setup_timetable = () => { + frappe.call({ + method: "lms.lms.doctype.lms_batch.lms_batch.get_batch_timetable", + args: { + batch: $(".class-details").data("batch"), + }, + callback: (r) => { + if (r.message.length) { + setup_calendar(r.message); + } + }, + }); +}; + +const setup_calendar = (events) => { + const element = $("#calendar"); + const Calendar = tui.Calendar; + let calendar_events = []; + let calendar_id = "calendar1"; + const container = element[0]; + const start_time = $(elemet).data("start"); + const end_time = $(elemet).data("end"); + + const options = { + defaultView: "week", + usageStatistics: false, + week: { + narrowWeekend: true, + hourStart: 7, + hourEnd: 18, + }, + month: { + narrowWeekend: true, + }, + taskView: false, + isReadOnly: true, + calendars: [ + { + id: calendar_id, + name: "Timetable", + backgroundColor: "#ffffff", + }, + ], + template: { + time: function (event) { + return `
+
${frappe.datetime.get_time(event.start.d.d)} - + ${frappe.datetime.get_time(event.end.d.d)}
+
${event.title}
+
`; + }, + }, + }; + const calendar = new Calendar(container, options); + this.calendar_ = calendar; + + events.forEach((event, idx) => { + let colors = get_background_color(event.reference_doctype); + calendar_events.push({ + id: `event${idx}`, + calendarId: calendar_id, + title: event.title, + start: `${event.date}T${event.start_time}`, + end: `${event.date}T${event.end_time}`, + isAllday: event.start_time ? false : true, + borderColor: colors.dark, + customStyle: { + borderRadius: "var(--border-radius-md)", + boxShadow: "var(--shadow-base)", + borderWidth: "8px", + padding: "1rem", + }, + raw: { + url: event.url, + }, + }); + }); + + calendar.createEvents(calendar_events); + + calendar.on("clickEvent", ({ event }) => { + const el = document.getElementById("clicked-event"); + window.open(event.raw.url, "_blank"); + }); + + if (new Date().getMonth() < new Date(events[0].date).getMonth()) { + calendar.setDate(new Date(events[0].date)); + } + + let week_start = frappe.datetime.global_date_format( + calendar.getDateRangeStart().d.d + ); + let week_end = frappe.datetime.global_date_format( + calendar.getDateRangeEnd().d.d + ); + $(".calendar-range").text(`${week_start} - ${week_end}`); +}; + +const get_background_color = (doctype) => { + if (doctype == "Course Lesson") + return { + light: "var(--blue-50)", + dark: "var(--blue-400)", + }; + if (doctype == "LMS Quiz") + return { + light: "var(--green-50)", + dark: "var(--green-400)", + }; + if (doctype == "LMS Assignment") + return { + light: "var(--orange-50)", + dark: "var(--orange-400)", + }; + if (doctype == "LMS Live Class") + return { + light: "var(--red-50)", + dark: "var(--red-400)", + }; +}; diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index 05d7b001..2e76c786 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -1,6 +1,6 @@ from frappe import _ import frappe -from frappe.utils import getdate, cint +from frappe.utils import getdate, get_datetime from lms.www.utils import get_assessments, is_student from lms.lms.utils import ( has_course_moderator_role, @@ -89,7 +89,20 @@ def get_context(context): ) context.all_assignments = get_all_assignments(batch_name) context.all_quizzes = get_all_quizzes(batch_name) - context.flow = get_scheduled_flow(batch_name) + context.show_timetable = frappe.db.count( + "LMS Batch Timetable", + { + "parent": batch_name, + }, + ) + print( + frappe.db.count( + "LMS Batch Timetable", + { + "parent": batch_name, + }, + ) + ) def get_all_quizzes(batch_name): @@ -210,38 +223,6 @@ def sort_students(batch_students): return batch_students -def get_scheduled_flow(batch_name): - chapters = [] - - lessons = frappe.get_all( - "Scheduled Flow", - {"parent": batch_name}, - ["name", "lesson", "date", "start_time", "end_time"], - order_by="idx", - ) - - for lesson in lessons: - lesson = get_lesson_details(lesson, batch_name) - chapter_exists = [ - chapter for chapter in chapters if chapter.chapter == lesson.chapter - ] - - if len(chapter_exists) == 0: - chapters.append( - frappe._dict( - { - "chapter": lesson.chapter, - "chapter_title": frappe.db.get_value("Course Chapter", lesson.chapter, "title"), - "lessons": [lesson], - } - ) - ) - else: - chapter_exists[0]["lessons"].append(lesson) - - return chapters - - def get_lesson_details(lesson, batch_name): lesson.update( frappe.db.get_value( diff --git a/lms/www/batches/index.py b/lms/www/batches/index.py index c76154d1..273af9d2 100644 --- a/lms/www/batches/index.py +++ b/lms/www/batches/index.py @@ -31,7 +31,6 @@ def get_context(context): batch.seats_left = ( batch.seat_count - batch.student_count if batch.seat_count else None ) - print(batch.name, batch.published) if not batch.published: private_batches.append(batch) elif getdate(batch.start_date) < getdate(): diff --git a/lms/www/utils.py b/lms/www/utils.py index 7220e39b..b44c6aec 100644 --- a/lms/www/utils.py +++ b/lms/www/utils.py @@ -108,6 +108,8 @@ def get_assignment_details(assessment, member): f"/assignment-submission/{assessment.assessment_name}/{submission_name}" ) + return assessment + def get_quiz_details(assessment, member): assessment.title = frappe.db.get_value("LMS Quiz", assessment.assessment_name, "title") @@ -131,6 +133,8 @@ def get_quiz_details(assessment, member): ) assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}" + return assessment + def is_student(batch, member=None): if not member: From 8098532215c4c76c5e9b9ffd593bd8c7bd86b78b Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 18 Sep 2023 19:16:57 +0530 Subject: [PATCH 016/112] feat: timetable legends and template --- lms/lms/doctype/lms_batch/lms_batch.js | 51 ++++++--- lms/lms/doctype/lms_batch/lms_batch.json | 15 +-- .../lms_timetable_template/__init__.py | 0 .../lms_timetable_template.js | 20 ++++ .../lms_timetable_template.json | 65 +++++++++++ .../lms_timetable_template.py | 9 ++ .../test_lms_timetable_template.py | 9 ++ lms/public/css/style.css | 29 ++++- lms/www/batches/batch.html | 29 ++--- lms/www/batches/batch.js | 102 +++++++++++------- lms/www/batches/batch.py | 30 ++++-- 11 files changed, 266 insertions(+), 93 deletions(-) create mode 100644 lms/lms/doctype/lms_timetable_template/__init__.py create mode 100644 lms/lms/doctype/lms_timetable_template/lms_timetable_template.js create mode 100644 lms/lms/doctype/lms_timetable_template/lms_timetable_template.json create mode 100644 lms/lms/doctype/lms_timetable_template/lms_timetable_template.py create mode 100644 lms/lms/doctype/lms_timetable_template/test_lms_timetable_template.py diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index a4277bd2..5669fe3f 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -26,22 +26,41 @@ frappe.ui.form.on("LMS Batch", { }); }, - fetch_lessons: (frm) => { - frappe.call({ - method: "lms.lms.doctype.lms_batch.lms_batch.fetch_lessons", - args: { - courses: frm.doc.courses, - }, - callback: (r) => { - if (r.message) { - r.message.forEach((lesson) => { - let row = frm.add_child("timetable"); - row.lesson = lesson.name; - row.lesson_title = lesson.title; + timetable_template: function (frm) { + if (frm.doc.timetable_template) { + frm.clear_table("timetable"); + frm.refresh_fields(); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "LMS Batch Timetable", + parent: "LMS Timetable Template", + fields: [ + "reference_doctype", + "reference_docname", + "date", + "start_time", + "end_time", + ], + filters: { + parent: frm.doc.timetable_template, + }, + order_by: "idx", + }, + callback: (data) => { + data.message.forEach((row) => { + let child = frm.add_child("timetable"); + child.reference_doctype = row.reference_doctype; + child.reference_docname = row.reference_docname; + child.date = row.date; + child.start_time = row.start_time; + child.end_time = row.end_time; }); - frm.refresh_field("scheduled_flow"); - } - }, - }); + frm.refresh_field("timetable"); + frm.save(); + }, + }); + } }, }); diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index 958cbb47..038239e7 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -35,7 +35,7 @@ "assessment_tab", "assessment", "schedule_tab", - "fetch_lessons", + "timetable_template", "timetable" ], "fields": [ @@ -150,11 +150,6 @@ "fieldname": "section_break_ubxi", "fieldtype": "Section Break" }, - { - "fieldname": "fetch_lessons", - "fieldtype": "Button", - "label": "Fetch Lessons" - }, { "fieldname": "schedule_tab", "fieldtype": "Tab Break", @@ -199,11 +194,17 @@ "fieldtype": "Table", "label": "Timetable", "options": "LMS Batch Timetable" + }, + { + "fieldname": "timetable_template", + "fieldtype": "Link", + "label": "Timetable Template", + "options": "LMS Timetable Template" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-14 12:51:11.847853", + "modified": "2023-09-18 17:36:03.621651", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch", diff --git a/lms/lms/doctype/lms_timetable_template/__init__.py b/lms/lms/doctype/lms_timetable_template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js new file mode 100644 index 00000000..d76abc7c --- /dev/null +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js @@ -0,0 +1,20 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("LMS Timetable Template", { + refresh(frm) { + frm.set_query("reference_doctype", "timetable", function () { + let doctypes = [ + "Course Lesson", + "LMS Quiz", + "LMS Assignment", + "LMS Live Class", + ]; + return { + filters: { + name: ["in", doctypes], + }, + }; + }); + }, +}); diff --git a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json new file mode 100644 index 00000000..99c88628 --- /dev/null +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json @@ -0,0 +1,65 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2023-09-18 14:16:16.964077", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "timetable" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "timetable", + "fieldtype": "Table", + "label": "Timetable", + "options": "LMS Batch Timetable" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-09-18 17:57:15.819072", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Timetable Template", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Moderator", + "share": 1, + "write": 1 + } + ], + "show_title_field_in_link": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.py b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.py new file mode 100644 index 00000000..21e7c70d --- /dev/null +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LMSTimetableTemplate(Document): + pass diff --git a/lms/lms/doctype/lms_timetable_template/test_lms_timetable_template.py b/lms/lms/doctype/lms_timetable_template/test_lms_timetable_template.py new file mode 100644 index 00000000..090d1a58 --- /dev/null +++ b/lms/lms/doctype/lms_timetable_template/test_lms_timetable_template.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSTimetableTemplate(FrappeTestCase): + pass diff --git a/lms/public/css/style.css b/lms/public/css/style.css index ada6eb38..e79eec3b 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -2414,7 +2414,32 @@ select { } .calendar-event-title { - font-size: var(--text-base); + font-size: var(--text-md); font-weight: 500; - margin-top: 0.25rem; + margin-top: 0.2rem; +} + +.legend-color { + width: 50px; + height: 20px; + border-radius: var(--border-radius-sm); + margin-right: 0.25rem; +} + +.legend-item { + display: flex; + align-items: center; +} + +.legend-text { + color: var(--text-color); + font-weight: 500; +} + +.calendar-legends { + display: flex; + align-items: center; + justify-content: space-between; + width: 50%; + margin: 0 auto 1rem; } \ No newline at end of file diff --git a/lms/www/batches/batch.html b/lms/www/batches/batch.html index 2cf378d0..bbfa676c 100644 --- a/lms/www/batches/batch.html +++ b/lms/www/batches/batch.html @@ -88,7 +88,7 @@
+
+ {% for legend in legends %} +
+
+
{{ legend.title }}
+
+ {% endfor %} +
- {% endmacro %} diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index f8cfbc8a..8dfc84a9 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -9,12 +9,14 @@ frappe.ready(() => { if ($("#calendar").length) { $(document).on("click", "#prev-week", (e) => { this.calendar_ && this.calendar_.prev(); + set_calendar_range(this.calendar_, this.events); }); } if ($("#calendar").length) { $(document).on("click", "#next-week", (e) => { this.calendar_ && this.calendar_.next(); + set_calendar_range(this.calendar_, this.events); }); } @@ -624,6 +626,7 @@ const submit_evaluation_form = (values) => { }; const setup_timetable = () => { + let self = this; frappe.call({ method: "lms.lms.doctype.lms_batch.lms_batch.get_batch_timetable", args: { @@ -632,6 +635,7 @@ const setup_timetable = () => { callback: (r) => { if (r.message.length) { setup_calendar(r.message); + self.events = r.message; } }, }); @@ -640,19 +644,29 @@ const setup_timetable = () => { const setup_calendar = (events) => { const element = $("#calendar"); const Calendar = tui.Calendar; - let calendar_events = []; - let calendar_id = "calendar1"; + const calendar_id = "calendar1"; const container = element[0]; - const start_time = $(elemet).data("start"); - const end_time = $(elemet).data("end"); + 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); + set_calendar_range(calendar, events); +}; - const options = { +const get_calendar_options = (element, calendar_id) => { + const start_time = element.data("start"); + const end_time = element.data("end"); + + return { defaultView: "week", usageStatistics: false, week: { narrowWeekend: true, - hourStart: 7, - hourEnd: 18, + hourStart: parseInt(start_time.split(":")[0]) - 1, + /* hourEnd: parseInt(end_time.split(":")[0]) + 1, */ }, month: { narrowWeekend: true, @@ -663,7 +677,7 @@ const setup_calendar = (events) => { { id: calendar_id, name: "Timetable", - backgroundColor: "#ffffff", + backgroundColor: "var(--fg-color)", }, ], template: { @@ -676,11 +690,12 @@ const setup_calendar = (events) => { }, }, }; - const calendar = new Calendar(container, options); - this.calendar_ = calendar; +}; + +const create_events = (calendar, events, calendar_id) => { + let calendar_events = []; events.forEach((event, idx) => { - let colors = get_background_color(event.reference_doctype); calendar_events.push({ id: `event${idx}`, calendarId: calendar_id, @@ -688,12 +703,13 @@ const setup_calendar = (events) => { start: `${event.date}T${event.start_time}`, end: `${event.date}T${event.end_time}`, isAllday: event.start_time ? false : true, - borderColor: colors.dark, + borderColor: get_background_color(event.reference_doctype), + backgroundColor: "var(--fg-color)", customStyle: { borderRadius: "var(--border-radius-md)", boxShadow: "var(--shadow-base)", borderWidth: "8px", - padding: "1rem", + padding: "0.25rem 0.5rem 0.5rem", }, raw: { url: event.url, @@ -702,44 +718,50 @@ const setup_calendar = (events) => { }); calendar.createEvents(calendar_events); +}; +const add_links_to_events = (calendar, events) => { calendar.on("clickEvent", ({ event }) => { const el = document.getElementById("clicked-event"); window.open(event.raw.url, "_blank"); }); +}; - if (new Date().getMonth() < new Date(events[0].date).getMonth()) { +const scroll_to_date = (calendar, events) => { + if ( + new Date() < new Date(events[0].date) || + new Date() > new Date(events.slice(-1).date) + ) { calendar.setDate(new Date(events[0].date)); } +}; - let week_start = frappe.datetime.global_date_format( - calendar.getDateRangeStart().d.d +const set_calendar_range = (calendar, events) => { + let week_start = moment(calendar.getDateRangeStart().d.d); + let week_end = moment(calendar.getDateRangeEnd().d.d); + + $(".calendar-range").text( + `${moment(week_start).format("DD MMMM YYYY")} - ${moment( + week_end + ).format("DD MMMM YYYY")}` ); - let week_end = frappe.datetime.global_date_format( - calendar.getDateRangeEnd().d.d - ); - $(".calendar-range").text(`${week_start} - ${week_end}`); + + if (week_start.diff(moment(events[0].date), "days") <= 0) { + $("#prev-week").hide(); + } else { + $("#prev-week").show(); + } + + if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) { + $("#next-week").hide(); + } else { + $("#next-week").show(); + } }; const get_background_color = (doctype) => { - if (doctype == "Course Lesson") - return { - light: "var(--blue-50)", - dark: "var(--blue-400)", - }; - if (doctype == "LMS Quiz") - return { - light: "var(--green-50)", - dark: "var(--green-400)", - }; - if (doctype == "LMS Assignment") - return { - light: "var(--orange-50)", - dark: "var(--orange-400)", - }; - if (doctype == "LMS Live Class") - return { - light: "var(--red-50)", - dark: "var(--red-400)", - }; + if (doctype == "Course Lesson") return "var(--blue-400)"; + if (doctype == "LMS Quiz") return "var(--green-400)"; + if (doctype == "LMS Assignment") return "var(--orange-400)"; + if (doctype == "LMS Live Class") return "var(--purple-400)"; }; diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index 2e76c786..00aac227 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -95,14 +95,7 @@ def get_context(context): "parent": batch_name, }, ) - print( - frappe.db.count( - "LMS Batch Timetable", - { - "parent": batch_name, - }, - ) - ) + context.legends = get_legends() def get_all_quizzes(batch_name): @@ -258,3 +251,24 @@ def get_course_progress(batch_courses, student_details): student_details.courses[course.course] = membership.progress else: student_details.courses[course.course] = 0 + + +def get_legends(): + return [ + { + "title": "Lesson", + "color": "var(--blue-400)", + }, + { + "title": "Quiz", + "color": "var(--green-400)", + }, + { + "title": "Assignment", + "color": "var(--orange-400)", + }, + { + "title": "Live Class", + "color": "var(--purple-400)", + }, + ] From 0d9926910954a80e1217526937279301efcc85d4 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 20 Sep 2023 12:09:02 +0530 Subject: [PATCH 017/112] feat: live class checkbox --- lms/lms/doctype/lms_batch/lms_batch.js | 7 +--- lms/lms/doctype/lms_batch/lms_batch.json | 19 ++++++++++- lms/lms/doctype/lms_batch/lms_batch.py | 34 +++++++++++++++---- .../lms_live_class/lms_live_class.json | 14 ++++---- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index 5669fe3f..b42ec2a4 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -12,12 +12,7 @@ frappe.ui.form.on("LMS Batch", { }); frm.set_query("reference_doctype", "timetable", function () { - let doctypes = [ - "Course Lesson", - "LMS Quiz", - "LMS Assignment", - "LMS Live Class", - ]; + let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"]; return { filters: { name: ["in", doctypes], diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index 038239e7..2f916303 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -36,6 +36,9 @@ "assessment", "schedule_tab", "timetable_template", + "column_break_anya", + "show_live_class", + "section_break_ontp", "timetable" ], "fields": [ @@ -200,11 +203,25 @@ "fieldtype": "Link", "label": "Timetable Template", "options": "LMS Timetable Template" + }, + { + "fieldname": "column_break_anya", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "show_live_class", + "fieldtype": "Check", + "label": "Show Live Class" + }, + { + "fieldname": "section_break_ontp", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-18 17:36:03.621651", + "modified": "2023-09-20 11:25:10.683688", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch", diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index 10664dd6..6cfbb6dc 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -6,6 +6,7 @@ import requests import base64 import json from frappe import _ +from datetime import timedelta from frappe.model.document import Document from frappe.utils import cint, format_date, format_datetime from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url @@ -274,6 +275,32 @@ def get_batch_timetable(batch): order_by="date", ) + show_live_class = frappe.db.get_value("LMS Batch", batch, "show_live_class") + if show_live_class: + live_classes = get_live_classes(batch) + timetable.extend(live_classes) + + timetable = get_timetable_details(timetable) + return timetable + + +def get_live_classes(batch): + live_classes = frappe.get_all( + "LMS Live Class", + {"batch_name": batch}, + ["name", "title", "date", "time as start_time", "duration", "join_url as url"], + order_by="date", + ) + for class_ in live_classes: + class_.end_time = class_.start_time + timedelta(minutes=class_.duration) + class_.reference_doctype = "LMS Live Class" + class_.reference_docname = class_.name + class_.icon = "icon-call" + + return live_classes + + +def get_timetable_details(timetable): for entry in timetable: entry.title = frappe.db.get_value( entry.reference_doctype, entry.reference_docname, "title" @@ -298,10 +325,5 @@ def get_batch_timetable(batch): details = get_assignment_details(assessment, frappe.session.user) entry.update(details) - elif entry.reference_doctype == "LMS Live Class": - entry.icon = "icon-call" - entry.url = frappe.db.get_value( - entry.reference_doctype, entry.reference_docname, "join_url" - ) - + timetable = sorted(timetable, key=lambda k: k["date"]) return timetable diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.json b/lms/lms/doctype/lms_live_class/lms_live_class.json index af87c823..74e21479 100644 --- a/lms/lms/doctype/lms_live_class/lms_live_class.json +++ b/lms/lms/doctype/lms_live_class/lms_live_class.json @@ -10,16 +10,16 @@ "title", "host", "batch_name", - "password", - "auto_recording", "column_break_astv", - "description", - "section_break_glxh", "date", - "timezone", - "column_break_spvt", "time", "duration", + "section_break_glxh", + "description", + "column_break_spvt", + "timezone", + "password", + "auto_recording", "section_break_yrpq", "start_url", "column_break_yokr", @@ -126,7 +126,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-14 15:02:44.081474", + "modified": "2023-09-20 11:29:20.899897", "modified_by": "Administrator", "module": "LMS", "name": "LMS Live Class", From 33f4e82399ea5616dd76b0a39db4f018b4ee8477 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 20 Sep 2023 12:40:04 +0530 Subject: [PATCH 018/112] fix: batch copy --- lms/www/batches/batch_details.html | 4 ++-- lms/www/batches/batch_details.py | 7 +++++-- lms/www/billing/billing.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lms/www/batches/batch_details.html b/lms/www/batches/batch_details.html index 5ed9fe76..b1967306 100644 --- a/lms/www/batches/batch_details.html +++ b/lms/www/batches/batch_details.html @@ -136,9 +136,9 @@ {% endif %}
- {% if is_moderator or is_evaluator or is_student %} + {% if is_moderator or is_evaluator %} - {{ _("Checkout Batch") }} + {{ _("Manage Batch") }} {% elif batch_info.paid_batch %} Date: Wed, 20 Sep 2023 13:00:51 +0530 Subject: [PATCH 019/112] fix: removed live class from template row --- lms/lms/doctype/lms_batch/lms_batch.js | 24 +++++++++++-------- .../lms_timetable_template.js | 7 +----- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index b42ec2a4..3028e564 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -44,18 +44,22 @@ frappe.ui.form.on("LMS Batch", { order_by: "idx", }, callback: (data) => { - data.message.forEach((row) => { - let child = frm.add_child("timetable"); - child.reference_doctype = row.reference_doctype; - child.reference_docname = row.reference_docname; - child.date = row.date; - child.start_time = row.start_time; - child.end_time = row.end_time; - }); - frm.refresh_field("timetable"); - frm.save(); + add_timetable_rows(frm, data.message); }, }); } }, }); + +const add_timetable_rows = (frm, timetable) => { + timetable.forEach((row) => { + let child = frm.add_child("timetable"); + child.reference_doctype = row.reference_doctype; + child.reference_docname = row.reference_docname; + child.date = row.date; + child.start_time = row.start_time; + child.end_time = row.end_time; + }); + frm.refresh_field("timetable"); + frm.save(); +}; diff --git a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js index d76abc7c..a9bff04e 100644 --- a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js @@ -4,12 +4,7 @@ frappe.ui.form.on("LMS Timetable Template", { refresh(frm) { frm.set_query("reference_doctype", "timetable", function () { - let doctypes = [ - "Course Lesson", - "LMS Quiz", - "LMS Assignment", - "LMS Live Class", - ]; + let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"]; return { filters: { name: ["in", doctypes], From 153a8428f7c9621afd0ccfdd141e73788080672a Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 21 Sep 2023 12:52:31 +0530 Subject: [PATCH 020/112] fix: billing flow --- lms/lms/doctype/lms_batch/lms_batch.js | 7 ++++++- lms/lms/doctype/lms_batch/lms_batch.json | 7 ++++--- lms/public/js/common_functions.js | 1 + lms/www/batches/batch_details.py | 13 ++++++++++++- lms/www/batches/index.py | 12 +++++++++++- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index 3028e564..07e88ed3 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -12,7 +12,12 @@ frappe.ui.form.on("LMS Batch", { }); frm.set_query("reference_doctype", "timetable", function () { - let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"]; + let doctypes = [ + "Course Lesson", + "LMS Quiz", + "LMS Assignment", + "LMS Live Class", + ]; return { filters: { name: ["in", doctypes], diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index 2f916303..d4e4ea6d 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -146,8 +146,9 @@ }, { "fieldname": "category", - "fieldtype": "Autocomplete", - "label": "Category" + "fieldtype": "Link", + "label": "Category", + "options": "LMS Category" }, { "fieldname": "section_break_ubxi", @@ -221,7 +222,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-20 11:25:10.683688", + "modified": "2023-09-20 14:40:45.940540", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch", diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index 8da2d052..59edd021 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -320,6 +320,7 @@ const open_batch_dialog = () => { label: __("Category"), fieldname: "category", options: "LMS Category", + only_select: 1, default: batch_info && batch_info.category, }, { diff --git a/lms/www/batches/batch_details.py b/lms/www/batches/batch_details.py index 91d11927..297a3bcb 100644 --- a/lms/www/batches/batch_details.py +++ b/lms/www/batches/batch_details.py @@ -1,6 +1,10 @@ import frappe from frappe import _ -from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role +from lms.lms.utils import ( + has_course_moderator_role, + has_course_evaluator_role, + check_multicurrency, +) from lms.www.utils import is_student @@ -29,6 +33,13 @@ def get_context(context): as_dict=1, ) + if context.batch_info.amount and context.batch_info.currency: + amount, currency = check_multicurrency( + context.batch_info.amount, context.batch_info.currency + ) + context.batch_info.amount = amount + context.batch_info.currency = currency + context.is_moderator = has_course_moderator_role() context.is_evaluator = has_course_evaluator_role() context.is_student = is_student(batch_name) diff --git a/lms/www/batches/index.py b/lms/www/batches/index.py index 273af9d2..292d7cab 100644 --- a/lms/www/batches/index.py +++ b/lms/www/batches/index.py @@ -1,6 +1,10 @@ import frappe from frappe.utils import getdate -from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role +from lms.lms.utils import ( + has_course_moderator_role, + has_course_evaluator_role, + check_multicurrency, +) def get_context(context): @@ -28,6 +32,12 @@ def get_context(context): for batch in batches: batch.student_count = frappe.db.count("Batch Student", {"parent": batch.name}) batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name}) + + if batch.amount and batch.currency: + amount, currency = check_multicurrency(batch.amount, batch.currency) + batch.amount = amount + batch.currency = currency + batch.seats_left = ( batch.seat_count - batch.student_count if batch.seat_count else None ) From 90587b05085d5bfd661be8c1720676b7a09a5f3c Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 22 Sep 2023 21:46:08 +0530 Subject: [PATCH 021/112] fix: price update on country --- lms/lms/utils.py | 36 ++++++++++++++++++-------- lms/public/css/style.css | 11 ++++++++ lms/www/batches/batch.py | 6 ++--- lms/www/batches/batch_details.html | 24 +++++++++-------- lms/www/batches/batch_details.py | 2 +- lms/www/billing/billing.html | 10 +++++++- lms/www/billing/billing.js | 41 ++++++++++++++++++++++++++++++ lms/www/billing/billing.py | 39 ++++++++++++++++++++++++++++ 8 files changed, 143 insertions(+), 26 deletions(-) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index ec3e4489..459a8337 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -18,6 +18,7 @@ from frappe.utils import ( get_datetime, getdate, validate_phone_number, + ceil, ) from frappe.utils.dateutils import get_period from lms.lms.md import find_macros, markdown_to_html @@ -842,7 +843,7 @@ def get_payment_options(doctype, docname, phone, country): validate_phone_number(phone, True) details = get_details(doctype, docname) details.amount, details.currency = check_multicurrency( - details.amount, details.currency + details.amount, details.currency, country ) if details.currency == "INR": details.amount, details.gst_applied = apply_gst(details.amount, country) @@ -866,18 +867,20 @@ def get_payment_options(doctype, docname, phone, country): return options -def check_multicurrency(amount, currency): +def check_multicurrency(amount, currency, country=None): show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent") exception_country = frappe.get_all( "Payment Country", filters={"parent": "LMS Settings"}, pluck="country" ) apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding") - country = frappe.db.get_value("User", frappe.session.user, "country") + country = country or frappe.db.get_value( + "Address", {"email_id": frappe.session.user}, "country" + ) if not show_usd_equivalent or currency == "USD": return amount, currency - if exception_country and country in exception_country: + if not country or (exception_country and country in exception_country): return amount, currency exchange_rate = get_current_exchange_rate(currency, "USD") @@ -885,7 +888,7 @@ def check_multicurrency(amount, currency): currency = "USD" if apply_rounding and amount % 100 != 0: - amount = amount + 100 - amount % 100 + amount = ceil(amount + 100 - amount % 100) return amount, currency @@ -928,7 +931,15 @@ def get_details(doctype, docname): def save_address(address): - address.update( + filters = {"email_id": frappe.session.user} + exists = frappe.db.exists("Address", filters) + if exists: + address_doc = frappe.get_last_doc("Address", filters=filters) + else: + address_doc = frappe.new_doc("Address") + + address_doc.update(address) + address_doc.update( { "address_title": frappe.db.get_value("User", frappe.session.user, "full_name"), "address_type": "Billing", @@ -936,10 +947,8 @@ def save_address(address): "email_id": frappe.session.user, } ) - doc = frappe.new_doc("Address") - doc.update(address) - doc.save(ignore_permissions=True) - return doc.name + address_doc.save(ignore_permissions=True) + return address_doc.name def get_client(): @@ -1062,3 +1071,10 @@ def get_current_exchange_rate(source, target="USD"): response = requests.request("GET", url) details = response.json() return details["rates"][target] + + +@frappe.whitelist() +def change_currency(amount, currency, country=None): + amount = cint(amount) + amount, currency = check_multicurrency(amount, currency, country) + return fmt_money(amount, 0, currency) diff --git a/lms/public/css/style.css b/lms/public/css/style.css index e79eec3b..a6ea778b 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -2442,4 +2442,15 @@ select { justify-content: space-between; width: 50%; margin: 0 auto 1rem; +} + +.batch-details { + width: 50%; + margin: 2rem 0; +} + +@media (max-width: 1000px) { + .batch-details { + width: 100%; + } } \ No newline at end of file diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index 00aac227..15d1b9d4 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -1,6 +1,6 @@ from frappe import _ import frappe -from frappe.utils import getdate, get_datetime +from frappe.utils import getdate from lms.www.utils import get_assessments, is_student from lms.lms.utils import ( has_course_moderator_role, @@ -52,14 +52,14 @@ def get_context(context): "Batch Course", {"parent": batch_name}, ["name", "course", "title"], - order_by="creation desc", + order_by="idx", ) batch_students = frappe.get_all( "Batch Student", {"parent": batch_name}, ["name", "student", "student_name", "username"], - order_by="creation desc", + order_by="idx", ) context.batch_courses = get_class_course_details(batch_courses) diff --git a/lms/www/batches/batch_details.html b/lms/www/batches/batch_details.html index b1967306..8d1d9ee9 100644 --- a/lms/www/batches/batch_details.html +++ b/lms/www/batches/batch_details.html @@ -164,24 +164,26 @@ {% macro BatchDetails(batch_info) %} -
-
- {{ batch_info.batch_details }} -
+
+ {{ batch_info.batch_details }}
{% endmacro %} {% macro CourseList(courses) %}
- {% if is_moderator %} - - {% endif %} -
- {{ _("Courses") }} + +
+
+ {{ _("Courses") }} +
+ {% if is_moderator %} + + {% endif %}
+ {% if courses | length %}
{% for course in courses %} diff --git a/lms/www/batches/batch_details.py b/lms/www/batches/batch_details.py index 297a3bcb..df044a1e 100644 --- a/lms/www/batches/batch_details.py +++ b/lms/www/batches/batch_details.py @@ -55,7 +55,7 @@ def get_context(context): "Batch Course", {"parent": batch_name}, ["name as batch_course", "course", "title", "evaluator"], - order_by="creation desc", + order_by="idx", ) for course in context.courses: diff --git a/lms/www/billing/billing.html b/lms/www/billing/billing.html index d9c5a956..bf1c1213 100644 --- a/lms/www/billing/billing.html +++ b/lms/www/billing/billing.html @@ -37,7 +37,8 @@
- {{ _("Total Price: ") }} {{ frappe.utils.fmt_money(amount, 2, currency) }} + {{ _("Total Price: ") }} + {{ frappe.utils.fmt_money(amount, 2, currency) }}
{% if gst_applied %} @@ -64,4 +65,11 @@ {%- block script %} {{ super() }} + {% endblock %} diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index 1bb2e24f..5a5bc8a2 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -18,23 +18,27 @@ const setup_billing = () => { label: __("Billing Name"), fieldname: "billing_name", reqd: 1, + default: address && address.billing_name, }, { fieldtype: "Data", label: __("Address Line 1"), fieldname: "address_line1", reqd: 1, + default: address && address.address_line1, }, { fieldtype: "Data", label: __("Address Line 2"), fieldname: "address_line2", + default: address && address.address_line2, }, { fieldtype: "Data", label: __("City/Town"), fieldname: "city", reqd: 1, + default: address && address.city, }, { fieldtype: "Column Break", @@ -43,6 +47,7 @@ const setup_billing = () => { fieldtype: "Data", label: __("State/Province"), fieldname: "state", + default: address && address.state, }, { fieldtype: "Link", @@ -51,18 +56,24 @@ const setup_billing = () => { options: "Country", reqd: 1, only_select: 1, + default: address && address.country, + change: () => { + change_currency(); + }, }, { fieldtype: "Data", label: __("Postal Code"), fieldname: "pincode", reqd: 1, + default: address && address.pincode, }, { fieldtype: "Data", label: __("Phone Number"), fieldname: "phone", reqd: 1, + default: address && address.phone, }, { fieldtype: "Section Break", @@ -143,3 +154,33 @@ const handle_success = (response, doctype, docname, address, order_id) => { }, }); }; + +const change_currency = () => { + let country = this.billing.get_value("country"); + if (exception_country.includes(country)) { + update_price(original_price_formatted); + return; + } + frappe.call({ + method: "lms.lms.utils.change_currency", + args: { + country: country, + amount: amount, + currency: currency, + }, + callback: (data) => { + let current_price = $(".total-price").text(); + if (current_price != data.message) { + update_price(data.message); + } + }, + }); +}; + +const update_price = (price) => { + $(".total-price").text(price); + frappe.show_alert({ + message: "Total Price has been updated.", + indicator: "yellow", + }); +}; diff --git a/lms/www/billing/billing.py b/lms/www/billing/billing.py index e88af68c..9c6c49ca 100644 --- a/lms/www/billing/billing.py +++ b/lms/www/billing/billing.py @@ -14,10 +14,17 @@ def get_context(context): validate_access(doctype, docname, module) get_billing_details(context) + context.original_amount = context.amount + context.original_currency = context.currency + context.exception_country = frappe.get_all( + "Payment Country", filters={"parent": "LMS Settings"}, pluck="country" + ) + context.amount, context.currency = check_multicurrency( context.amount, context.currency ) + context.address = get_address() if context.currency == "INR": context.amount, context.gst_applied = apply_gst(context.amount, None) @@ -75,3 +82,35 @@ def get_billing_details(context): context.title = details.title context.amount = details.amount context.currency = details.currency + + +def get_address(): + address = frappe.get_all( + "Address", + {"email_id": frappe.session.user}, + [ + "address_title as billing_name", + "address_line1", + "address_line2", + "city", + "state", + "country", + "pincode", + "phone", + ], + order_by="creation desc", + limit=1, + ) + + if not len(address): + return None + else: + address = address[0] + + if not address.address_line2: + address.address_line2 = "" + + if not address.state: + address.state = "" + + return address From 23e6ebe8eede25070ef918f14dbeac3d2b088241 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 25 Sep 2023 17:50:36 +0530 Subject: [PATCH 022/112] fix: multicurrency on course pages --- lms/www/courses/course.py | 6 ++++++ lms/www/courses/index.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/lms/www/courses/course.py b/lms/www/courses/course.py index 2666739a..b4aa476c 100644 --- a/lms/www/courses/course.py +++ b/lms/www/courses/course.py @@ -10,6 +10,7 @@ from lms.lms.utils import ( is_instructor, redirect_to_courses_list, get_average_rating, + check_multicurrency, ) @@ -60,6 +61,11 @@ def set_course_context(context, course_name): as_dict=True, ) + if course.course_price: + course.course_price, course.currency = check_multicurrency( + course.course_price, course.currency + ) + if frappe.form_dict.get("edit"): if not is_instructor(course.name) and not has_course_moderator_role(): raise frappe.PermissionError(_("You do not have permission to access this page.")) diff --git a/lms/www/courses/index.py b/lms/www/courses/index.py index 407d58e3..d887e1ca 100644 --- a/lms/www/courses/index.py +++ b/lms/www/courses/index.py @@ -7,6 +7,7 @@ from lms.lms.utils import ( has_course_moderator_role, get_courses_under_review, get_average_rating, + check_multicurrency, ) from lms.overrides.user import get_enrolled_courses, get_authored_courses @@ -58,6 +59,12 @@ def get_courses(): course.enrollment_count = frappe.db.count( "LMS Enrollment", {"course": course.name, "member_type": "Student"} ) + + if course.course_price: + course.course_price, course.currency = check_multicurrency( + course.course_price, course.currency + ) + course.avg_rating = get_average_rating(course.name) or 0 if course.upcoming: upcoming_courses.append(course) From 5727b7cd73ce72037214d7b2d8d8fe4bcbd11697 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 25 Sep 2023 22:08:37 +0530 Subject: [PATCH 023/112] fix: sanitized inputs for people and course creation page --- lms/www/courses/create.js | 7 ++++++- lms/www/people/index.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lms/www/courses/create.js b/lms/www/courses/create.js index fcbb2e27..64984e32 100644 --- a/lms/www/courses/create.js +++ b/lms/www/courses/create.js @@ -51,8 +51,13 @@ const create_tag = (e) => { if ($(e.target).val() == "") { return; } + + let tag_value = $(e.target) + .val() + .replace(//g, ">"); let tag = `
{% if gst_applied %} - + {{ _("18% GST included") }} {% endif %} diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index 5a5bc8a2..3be9b310 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -105,7 +105,7 @@ const setup_billing = () => { }; const generate_payment_link = (e) => { - address = this.billing.get_values(); + let new_address = this.billing.get_values(); let doctype = $(e.currentTarget).attr("data-doctype"); let docname = decodeURIComponent($(e.currentTarget).attr("data-name")); @@ -114,8 +114,8 @@ const generate_payment_link = (e) => { args: { doctype: doctype, docname: docname, - phone: address.phone, - country: address.country, + phone: new_address.phone, + country: new_address.country, }, callback: (data) => { data.message.handler = (response) => { @@ -123,7 +123,7 @@ const generate_payment_link = (e) => { response, doctype, docname, - address, + new_address, data.message.order_id ); }; @@ -156,6 +156,7 @@ const handle_success = (response, doctype, docname, address, order_id) => { }; const change_currency = () => { + $("#gst-message").removeClass("hide"); let country = this.billing.get_value("country"); if (exception_country.includes(country)) { update_price(original_price_formatted); @@ -173,6 +174,9 @@ const change_currency = () => { if (current_price != data.message) { update_price(data.message); } + if (!data.message.includes("INR")) { + $("#gst-message").addClass("hide"); + } }, }); }; diff --git a/lms/www/billing/billing.py b/lms/www/billing/billing.py index 9c6c49ca..ad1d45d3 100644 --- a/lms/www/billing/billing.py +++ b/lms/www/billing/billing.py @@ -14,8 +14,7 @@ def get_context(context): validate_access(doctype, docname, module) get_billing_details(context) - context.original_amount = context.amount - context.original_currency = context.currency + context.exception_country = frappe.get_all( "Payment Country", filters={"parent": "LMS Settings"}, pluck="country" ) @@ -28,6 +27,9 @@ def get_context(context): if context.currency == "INR": context.amount, context.gst_applied = apply_gst(context.amount, None) + context.original_amount = context.amount + context.original_currency = context.currency + def validate_access(doctype, docname, module): if frappe.session.user == "Guest": From db71f1271b1f97a7d1c8e10a334ec27695a90e76 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 27 Sep 2023 17:59:36 +0530 Subject: [PATCH 025/112] feat: editor js for instructor notes --- .../doctype/course_lesson/course_lesson.json | 4 +- lms/lms/utils.py | 5 - lms/public/css/style.css | 16 ++ lms/www/batch/edit.html | 47 +++-- lms/www/batch/edit.js | 192 ++++++++++-------- lms/www/batch/learn.html | 45 +--- lms/www/batch/learn.py | 4 + 7 files changed, 174 insertions(+), 139 deletions(-) diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index 3fd75f37..4bd3c65d 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -135,13 +135,13 @@ }, { "fieldname": "instructor_notes", - "fieldtype": "Text", + "fieldtype": "Markdown Editor", "label": "Instructor Notes" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-31 21:47:06.314995", + "modified": "2023-09-27 15:45:54.738573", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index f66db678..dfefe26f 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -152,11 +152,6 @@ def get_lesson_details(chapter): ) lesson_details.number = flt(f"{chapter.idx}.{row.idx}") lesson_details.icon = get_lesson_icon(lesson_details.body) - if lesson_details.instructor_notes: - lesson_details.instructor_notes_html = markdown_to_html( - lesson_details.instructor_notes - ) - lessons.append(lesson_details) return lessons diff --git a/lms/public/css/style.css b/lms/public/css/style.css index a6ea778b..5cd1fb3f 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -160,6 +160,10 @@ textarea.field-input { position: unset; } +.codex-editor--narrow .ce-toolbar__actions { + right: 100%; +} + .lesson-editor { border: 1px solid var(--gray-300); border-radius: var(--border-radius-md); @@ -2453,4 +2457,16 @@ select { .batch-details { width: 100%; } +} + +.collapse-section { + font-size: var(--text-lg); + cursor: pointer; +} + +.collapse-section.collapsed .icon { + transition: all 0.5s; + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + transform: rotate(180deg); } \ No newline at end of file diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 641f5256..5f47b3a0 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -75,41 +75,54 @@
-
-
- {{ _("Instructor Notes") }} + -
- {{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }} + +
+
-
- {% if lesson.instructor_notes %} -
{{ lesson.instructor_notes }}
+ + {% if lesson.body %} +
{{ lesson.body }}
{% endif %}
-
+ +
+
+
+ {% if lesson.instructor_notes %} +
{{ lesson.instructor_notes }}
{% endif %}
- {% endmacro %} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index 69f4c463..9b124812 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -1,110 +1,129 @@ frappe.ready(() => { let self = this; - + frappe.require("controls.bundle.js"); frappe.telemetry.capture("on_lesson_creation_page", "lms"); - if ($("#instructor-notes").length) { - frappe.require("controls.bundle.js", () => { - make_instructor_notes_component(); - }); - } - if ($("#current-lesson-content").length) { - parse_string_to_lesson(); + parse_string_to_lesson("lesson"); } - setup_editor(); + if ($("#current-instructor-notes").length) { + parse_string_to_lesson("notes"); + } + + setup_editor_for_lesson_content(); + setup_editor_for_instructor_notes(); $("#save-lesson").click((e) => { save_lesson(e); }); }); -const setup_editor = () => { +const setup_editor_for_lesson_content = () => { self.editor = new EditorJS({ holder: "lesson-content", - tools: { - embed: { - class: Embed, - config: { - services: { - youtube: true, - vimeo: true, - codepen: true, - slides: { - regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/, - embedUrl: - "https://docs.google.com/presentation/d/e/<%= remote_id %>/embed", - html: "", - }, - pdf: { - regex: /(https?:\/\/.*\.pdf)/, - embedUrl: "<%= remote_id %>", - html: "", - }, - }, - }, - }, - header: { - class: Header, - inlineToolbar: ["bold", "italic", "link"], - config: { - levels: [4, 5, 6], - defaultLevel: 5, - }, - icon: ` - - `, - }, - paragraph: { - class: Paragraph, - inlineToolbar: true, - config: { - preserveBlank: true, - }, - }, - youtube: YouTubeVideo, - quiz: Quiz, - upload: Upload, - }, + tools: get_tools(), data: { - blocks: self.blocks ? self.blocks : [], + blocks: self.lesson_blocks || [], }, }); }; -const parse_string_to_lesson = () => { - let lesson_content = $("#current-lesson-content").html(); - let lesson_blocks = []; +const setup_editor_for_instructor_notes = () => { + self.instructor_notes_editor = new EditorJS({ + holder: "instructor-notes", + tools: get_tools(), + data: { + blocks: self.notes_blocks || [], + }, + }); +}; - lesson_content.split("\n").forEach((block) => { +const get_tools = () => { + return { + embed: { + class: Embed, + config: { + services: { + youtube: true, + vimeo: true, + codepen: true, + slides: { + regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/, + embedUrl: + "https://docs.google.com/presentation/d/e/<%= remote_id %>/embed", + html: "", + }, + pdf: { + regex: /(https?:\/\/.*\.pdf)/, + embedUrl: "<%= remote_id %>", + html: "", + }, + }, + }, + }, + header: { + class: Header, + inlineToolbar: ["bold", "italic", "link"], + config: { + levels: [4, 5, 6], + defaultLevel: 5, + }, + icon: ` + + `, + }, + paragraph: { + class: Paragraph, + inlineToolbar: true, + config: { + preserveBlank: true, + }, + }, + youtube: YouTubeVideo, + quiz: Quiz, + upload: Upload, + }; +}; + +const parse_string_to_lesson = (type) => { + let content; + let blocks = []; + + if (type == "lesson") { + content = $("#current-lesson-content").html(); + } else if (type == "notes") { + content = $("#current-instructor-notes").html(); + } + + content.split("\n").forEach((block) => { if (block.includes("{{ YouTubeVideo")) { - let youtube_id = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let youtube_id = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "youtube", data: { youtube: youtube_id, }, }); } else if (block.includes("{{ Quiz")) { - let quiz = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let quiz = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "quiz", data: { quiz: quiz, }, }); } else if (block.includes("{{ Video")) { - let video = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let video = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "upload", data: { file_url: video, }, }); } else if (block.includes("{{ Embed")) { - let embed = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let embed = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "embed", data: { service: embed.split("|||")[0], @@ -113,7 +132,7 @@ const parse_string_to_lesson = () => { }); } else if (block.includes("![]")) { let image = block.match(/\((.*?)\)/)[1]; - lesson_blocks.push({ + blocks.push({ type: "upload", data: { file_url: image, @@ -121,7 +140,7 @@ const parse_string_to_lesson = () => { }); } else if (block.includes("#")) { let level = (block.match(/#/g) || []).length; - lesson_blocks.push({ + blocks.push({ type: "header", data: { text: block.replace(/#/g, "").trim(), @@ -129,7 +148,7 @@ const parse_string_to_lesson = () => { }, }); } else { - lesson_blocks.push({ + blocks.push({ type: "paragraph", data: { text: block, @@ -138,16 +157,25 @@ const parse_string_to_lesson = () => { } }); - this.blocks = lesson_blocks; + if (type == "lesson") { + this.lesson_blocks = blocks; + } else if (type == "notes") { + this.notes_blocks = blocks; + } }; const save_lesson = (e) => { self.editor.save().then((outputData) => { - parse_lesson_to_string(outputData); + parse_content_to_string(outputData, "lesson"); + + self.instructor_notes_editor.save().then((outputData) => { + parse_content_to_string(outputData, "notes"); + save(); + }); }); }; -const parse_lesson_to_string = (data) => { +const parse_content_to_string = (data, type) => { let lesson_content = ""; data.blocks.forEach((block) => { if (block.type == "youtube") { @@ -175,24 +203,28 @@ const parse_lesson_to_string = (data) => { }|||${block.data.embed.replace(/&/g, "&")}") }}\n`; } }); - save(lesson_content); + if (type == "lesson") { + this.lesson_content_data = lesson_content; + } else if (type == "notes") { + this.instructor_notes_data = lesson_content; + } }; -const save = (lesson_content) => { - validate_mandatory(lesson_content); +const save = () => { + console.log(this.instructor_notes_data); + console.log(this.lesson_content_data); + validate_mandatory(this.lesson_content_data); let lesson = $("#lesson-title").data("lesson"); - frappe.call({ method: "lms.lms.doctype.lms_course.lms_course.save_lesson", args: { title: $("#lesson-title").val(), - body: lesson_content, + body: this.lesson_content_data, chapter: $("#lesson-title").data("chapter"), preview: $("#preview").prop("checked") ? 1 : 0, idx: $("#lesson-title").data("index"), lesson: lesson ? lesson : "", - instructor_notes: - this.instructor_notes.get_values().instructor_notes, + instructor_notes: this.instructor_notes_data, }, callback: (data) => { frappe.show_alert({ diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index a4720d3a..7c34b3b6 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -156,12 +156,17 @@ {% endif %} {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} -
-
- {{ _("Instructor Notes") }} +
+ -
- {{ lesson.instructor_notes_html }} +
+ {{ instructor_notes }}
{% endif %} @@ -227,36 +232,6 @@
{% endmacro %} - - -{% macro HelpArticle() %} -
-

{{ _("Embed Components") }}

-

- {{ _("You can add additional content to the lesson using a special syntax. The table below mentions - all types of dynamic content that you can add to the lessons and the syntax for the same.") }} -

-
    -
  1. - {{ _("YouTube Video") }} -

    To get the YouTube Video ID, follow the steps mentioned below.

    -
      -
    • - {{ _("Upload the video on youtube.") }} -
    • -
    • - {{ _("When you share a youtube video, it shows a URL") }} -
    • -
    • - {{ _("Copy the last parameter of the URL and paste it here.") }} -
    • -
    -
  2. -
-
-{% endmacro %} - - {% macro Discussions() %} {% set topics_count = frappe.db.count("Discussion Topic", { diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index c1fde031..c77233b7 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -1,6 +1,7 @@ import frappe from frappe import _ from frappe.utils import cstr, flt +from lms.lms.md import markdown_to_html from lms.lms.utils import ( get_lesson_url, @@ -46,6 +47,9 @@ def get_context(context): context.is_moderator = has_course_moderator_role() context.is_evaluator = has_course_evaluator_role() + if context.lesson.instructor_notes: + context.instructor_notes = markdown_to_html(context.lesson.instructor_notes) + context.show_lesson = ( context.membership or (context.lesson and context.lesson.include_in_preview) From 0fcea692c71f4ef7fe472bc6c26cad23ff07d833 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 27 Sep 2023 19:21:57 +0530 Subject: [PATCH 026/112] feat: batch meta and raw details --- lms/lms/doctype/lms_batch/lms_batch.json | 24 +++++++++++++++++++++++- lms/lms/doctype/lms_batch/lms_batch.py | 4 ++++ lms/public/js/common_functions.js | 12 ++++++++++++ lms/www/batches/batch_details.html | 10 ++++++++++ lms/www/batches/batch_details.py | 10 ++++++++++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index d4e4ea6d..beb3c22a 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -22,7 +22,11 @@ "seat_count", "section_break_6", "description", + "batch_details_raw", + "column_break_hlqw", "batch_details", + "meta_image", + "section_break_jgji", "students", "courses", "section_break_gsac", @@ -218,11 +222,29 @@ { "fieldname": "section_break_ontp", "fieldtype": "Section Break" + }, + { + "fieldname": "batch_details_raw", + "fieldtype": "HTML Editor", + "label": "Batch Details Raw" + }, + { + "fieldname": "column_break_hlqw", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_jgji", + "fieldtype": "Section Break" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Meta Image" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-20 14:40:45.940540", + "modified": "2023-09-27 18:42:53.301107", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch", diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index f7e824d7..5213f93e 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -194,6 +194,8 @@ def create_batch( end_date, description=None, batch_details=None, + batch_details_raw=None, + meta_image=None, seat_count=0, start_time=None, end_time=None, @@ -218,6 +220,8 @@ def create_batch( "end_date": end_date, "description": description, "batch_details": batch_details, + "batch_details_raw": batch_details_raw, + "image": meta_image, "seat_count": seat_count, "start_time": start_time, "end_time": end_time, diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index 59edd021..d0f17265 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -340,6 +340,18 @@ const open_batch_dialog = () => { default: batch_info && batch_info.batch_details, reqd: 1, }, + { + fieldtype: "HTML Editor", + label: __("Batch Details Raw"), + fieldname: "batch_details_raw", + default: batch_info && batch_info.batch_details_raw, + }, + { + fieldtype: "Attach Image", + label: __("Meta Image"), + fieldname: "meta_image", + default: batch_info && batch_info.image, + }, { fieldtype: "Section Break", label: __("Pricing"), diff --git a/lms/www/batches/batch_details.html b/lms/www/batches/batch_details.html index 8d1d9ee9..d543bc88 100644 --- a/lms/www/batches/batch_details.html +++ b/lms/www/batches/batch_details.html @@ -14,6 +14,7 @@ {{ CourseList(courses) }}
+ {{ BatchDetailsRaw() }}
{% endblock %} @@ -216,6 +217,15 @@
{% endmacro %} + +{% macro BatchDetailsRaw() %} + {% if batch_info.batch_details_raw %} +
+ {{ batch_info.batch_details_raw }} +
+ {% endif %} +{% endmacro %} + {%- block script %} {{ super() }} {% if is_moderator %} diff --git a/lms/www/batches/batch_details.py b/lms/www/batches/batch_details.py index df044a1e..8840a91d 100644 --- a/lms/www/batches/batch_details.py +++ b/lms/www/batches/batch_details.py @@ -29,6 +29,8 @@ def get_context(context): "end_time", "seat_count", "published", + "meta_image", + "batch_details_raw", ], as_dict=1, ) @@ -67,3 +69,11 @@ def get_context(context): context.student_count = frappe.db.count("Batch Student", {"parent": batch_name}) context.seats_left = context.batch_info.seat_count - context.student_count + + context.metatags = { + "title": context.batch_info.title, + "image": context.batch_info.meta_image, + "description": context.batch_info.description, + "keywords": context.batch_info.title, + "og:type": "website", + } From d5387a0d1ab749c5519eedf8f570d3af1be40676 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 27 Sep 2023 19:57:37 +0530 Subject: [PATCH 027/112] test: fix course creation test --- cypress/e2e/course_creation.cy.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 48f3c5dd..98ae26cc 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -33,15 +33,16 @@ describe("Course Creation", () => { cy.get("#lesson-title").type("Test Lesson"); // Content - cy.get(".ce-block").click().type("{enter}"); - cy.get(".ce-toolbar__plus").click(); - cy.get('[data-item-name="youtube"]').click(); + cy.get(".collapse-section.collapsed:first").click(); + cy.get("#lesson-content .ce-block").click().type("{enter}"); + cy.get("#lesson-content .ce-toolbar__plus").click(); + cy.get('#lesson-content [data-item-name="youtube"]').click(); cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto"); cy.button("Insert").click(); cy.wait(1000); - cy.get(".ce-block:last").click().type("{enter}"); - cy.get(".ce-block:last") + cy.get("#lesson-content .ce-block:last").click().type("{enter}"); + cy.get("#lesson-content .ce-block:last") .click() .type( "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." From cc7832614b954c9c9f6450719f45ffc7dbc401d9 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 28 Sep 2023 10:14:10 +0530 Subject: [PATCH 028/112] fix: hide course header for students if no courses in batch --- lms/www/batches/batch_details.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lms/www/batches/batch_details.html b/lms/www/batches/batch_details.html index 8d1d9ee9..d8eeeb1a 100644 --- a/lms/www/batches/batch_details.html +++ b/lms/www/batches/batch_details.html @@ -171,6 +171,7 @@ {% macro CourseList(courses) %} +{% if courses | length or is_moderator %}
@@ -214,6 +215,7 @@
{% endif %}
+{% endif %} {% endmacro %} {%- block script %} From bf3c6bc6be1acd510000ed0ceddd3a670b352bdf Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 28 Sep 2023 10:39:30 +0530 Subject: [PATCH 029/112] test: change lesson sequence --- cypress/e2e/course_creation.cy.js | 13 +++++-------- lms/www/batch/learn.html | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 98ae26cc..2be9a71c 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -34,19 +34,16 @@ describe("Course Creation", () => { // Content cy.get(".collapse-section.collapsed:first").click(); - cy.get("#lesson-content .ce-block").click().type("{enter}"); + cy.get("#lesson-content .ce-block") + .click() + .type( + "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now. {enter}" + ); cy.get("#lesson-content .ce-toolbar__plus").click(); cy.get('#lesson-content [data-item-name="youtube"]').click(); cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto"); cy.button("Insert").click(); cy.wait(1000); - - cy.get("#lesson-content .ce-block:last").click().type("{enter}"); - cy.get("#lesson-content .ce-block:last") - .click() - .type( - "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." - ); cy.button("Save").click(); // View Course diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 7c34b3b6..60279848 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -155,7 +155,7 @@
{% endif %} - {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} + {% if instructor_notes and (is_moderator or instructor or is_evaluator) %}
{% endmacro %} @@ -150,6 +152,9 @@ {% endif %} + {% if custom_tabs_header %} + {% include custom_tabs_header %} + {% endif %}
@@ -192,6 +197,10 @@
{% endif %} + {% if custom_tabs_content %} + {% include custom_tabs_content %} + {% endif %} +
{% endmacro %} @@ -202,17 +211,19 @@ {% set assessments = current_student.assessments %} {% set student = current_student %} -{% if student.name == frappe.session.user %} - -{% endif %} +
+ {% if student.name == frappe.session.user %} + + {% endif %} -
- {% include "lms/templates/upcoming_evals.html" %} -
-
- {% include "lms/templates/assessments.html" %} +
+ {% include "lms/templates/upcoming_evals.html" %} +
+
+ {% include "lms/templates/assessments.html" %} +
{% endmacro %} @@ -421,7 +432,7 @@ {{ CreateLiveClass(batch_info) }} {{ LiveClassList(batch_info, live_classes) }} -
+ {% endmacro %} @@ -547,10 +558,17 @@ {%- block script %} {{ super() }} + {% if batch_info.custom_script %} + + {% endif %} + + {% endblock %} \ No newline at end of file diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index 15d1b9d4..f3b31d90 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -32,6 +32,7 @@ def get_context(context): "description", "medium", "custom_component", + "custom_script", "seat_count", "start_time", "end_time", @@ -97,6 +98,13 @@ def get_context(context): ) context.legends = get_legends() + custom_tabs = frappe.get_hooks("lms_batch_tabs") + + if custom_tabs: + context.custom_tabs_header = (custom_tabs.get("header_html")[0],) + context.custom_tabs_content = (custom_tabs.get("content_html")[0],) + context.update(frappe.get_attr(custom_tabs.get("context")[0])()) + def get_all_quizzes(batch_name): filters = {} if has_course_moderator_role() else {"owner": frappe.session.user} From be23220e01304a5742446e035be7d4e9ebeb877b Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 29 Sep 2023 17:14:00 +0530 Subject: [PATCH 031/112] feat: lms student role --- lms/install.py | 14 ++++++++++++ .../job_opportunity/job_opportunity.json | 4 ++-- lms/lms/api.py | 10 ++++----- .../cohort_join_request.json | 14 +++++++++++- .../course_chapter/course_chapter.json | 4 ++-- .../doctype/course_lesson/course_lesson.json | 4 ++-- lms/lms/doctype/function/function.json | 7 +++--- lms/lms/doctype/industry/industry.json | 7 +++--- .../lms_enrollment/lms_enrollment.json | 13 ++++++++++- lms/lms/doctype/user_skill/user_skill.json | 7 +++--- lms/lms/test_utils.py | 2 +- lms/lms/utils.py | 2 +- lms/patches.txt | 3 ++- lms/patches/v1_0/create_student_role.py | 22 +++++++++++++++++++ 14 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 lms/patches/v1_0/create_student_role.py diff --git a/lms/install.py b/lms/install.py index 5856b839..459d2c38 100644 --- a/lms/install.py +++ b/lms/install.py @@ -54,6 +54,7 @@ def create_lms_roles(): create_course_creator_role() create_moderator_role() create_evaluator_role() + create_lms_student_role() def delete_lms_roles(): @@ -106,6 +107,19 @@ def create_evaluator_role(): role.save() +def create_lms_student_role(): + if not frappe.db.exists("Role", "LMS Student"): + role = frappe.new_doc("Role") + role.update( + { + "role_name": "LMS Student", + "home_page": "", + "desk_access": 0, + } + ) + role.save() + + def set_default_certificate_print_format(): filters = { "doc_type": "LMS Certificate", diff --git a/lms/job/doctype/job_opportunity/job_opportunity.json b/lms/job/doctype/job_opportunity/job_opportunity.json index 7ac25e65..15bf16b3 100644 --- a/lms/job/doctype/job_opportunity/job_opportunity.json +++ b/lms/job/doctype/job_opportunity/job_opportunity.json @@ -116,7 +116,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2022-09-15 17:22:21.662675", + "modified": "2023-09-29 17:03:30.825021", "modified_by": "Administrator", "module": "Job", "name": "Job Opportunity", @@ -144,7 +144,7 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "LMS Student", "select": 1, "share": 1, "write": 1 diff --git a/lms/lms/api.py b/lms/lms/api.py index 480809bc..1e68205c 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -40,7 +40,7 @@ def save_current_lesson(course_name, lesson_name): return doc = frappe.get_doc("LMS Enrollment", name) doc.current_lesson = lesson_name - doc.save(ignore_permissions=True) + doc.save() return {"current_lesson": doc.current_lesson} @@ -66,7 +66,7 @@ def join_cohort(course, cohort, subgroup, invite_code): return {"ok": True, "status": "record found"} else: doc = frappe.get_doc(data) - doc.insert(ignore_permissions=True) + doc.insert() return {"ok": True, "status": "record created"} @@ -82,7 +82,7 @@ def approve_cohort_join_request(join_request): return {"ok": False, "error": "Permission Deined"} r.status = "Accepted" - r.save(ignore_permissions=True) + r.save() return {"ok": True} @@ -98,7 +98,7 @@ def reject_cohort_join_request(join_request): return {"ok": False, "error": "Permission Deined"} r.status = "Rejected" - r.save(ignore_permissions=True) + r.save() return {"ok": True} @@ -115,7 +115,7 @@ def undo_reject_cohort_join_request(join_request): return {"ok": False, "error": "Permission Deined"} r.status = "Pending" - r.save(ignore_permissions=True) + r.save() return {"ok": True} diff --git a/lms/lms/doctype/cohort_join_request/cohort_join_request.json b/lms/lms/doctype/cohort_join_request/cohort_join_request.json index 8ecd4be3..587d988c 100644 --- a/lms/lms/doctype/cohort_join_request/cohort_join_request.json +++ b/lms/lms/doctype/cohort_join_request/cohort_join_request.json @@ -51,7 +51,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-16 15:06:03.985221", + "modified": "2023-09-29 17:08:18.950560", "modified_by": "Administrator", "module": "LMS", "name": "Cohort Join Request", @@ -68,9 +68,21 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "LMS Student", + "share": 1, + "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lms/lms/doctype/course_chapter/course_chapter.json b/lms/lms/doctype/course_chapter/course_chapter.json index ee8544bf..ba73237f 100644 --- a/lms/lms/doctype/course_chapter/course_chapter.json +++ b/lms/lms/doctype/course_chapter/course_chapter.json @@ -59,7 +59,7 @@ "link_fieldname": "chapter" } ], - "modified": "2022-03-14 17:57:00.707416", + "modified": "2023-09-29 17:03:58.013819", "modified_by": "Administrator", "module": "LMS", "name": "Course Chapter", @@ -86,7 +86,7 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "LMS Student", "select": 1, "share": 1, "write": 1 diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index 4bd3c65d..d5879b00 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -141,7 +141,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-27 15:45:54.738573", + "modified": "2023-09-29 17:04:19.252897", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", @@ -169,7 +169,7 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "LMS Student", "select": 1, "share": 1, "write": 1 diff --git a/lms/lms/doctype/function/function.json b/lms/lms/doctype/function/function.json index 918ceb3e..40bf752e 100644 --- a/lms/lms/doctype/function/function.json +++ b/lms/lms/doctype/function/function.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-21 09:34:35.018280", + "modified": "2023-09-29 17:04:58.167481", "modified_by": "Administrator", "module": "LMS", "name": "Function", @@ -44,11 +44,12 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "LMS Student", "select": 1, "share": 1 } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/lms/lms/doctype/industry/industry.json b/lms/lms/doctype/industry/industry.json index 2ab23031..5bb770cb 100644 --- a/lms/lms/doctype/industry/industry.json +++ b/lms/lms/doctype/industry/industry.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-21 09:35:20.443192", + "modified": "2023-09-29 17:05:27.231982", "modified_by": "Administrator", "module": "LMS", "name": "Industry", @@ -44,11 +44,12 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "LMS Student", "select": 1, "share": 1 } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/lms/lms/doctype/lms_enrollment/lms_enrollment.json b/lms/lms/doctype/lms_enrollment/lms_enrollment.json index 2ebf9161..a0d191e9 100644 --- a/lms/lms/doctype/lms_enrollment/lms_enrollment.json +++ b/lms/lms/doctype/lms_enrollment/lms_enrollment.json @@ -123,7 +123,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-24 17:52:35.487141", + "modified": "2023-09-29 17:07:38.695936", "modified_by": "Administrator", "module": "LMS", "name": "LMS Enrollment", @@ -140,6 +140,17 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "LMS Student", + "share": 1, + "write": 1 } ], "quick_entry": 1, diff --git a/lms/lms/doctype/user_skill/user_skill.json b/lms/lms/doctype/user_skill/user_skill.json index ecc08a98..e047b414 100644 --- a/lms/lms/doctype/user_skill/user_skill.json +++ b/lms/lms/doctype/user_skill/user_skill.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-21 09:35:44.265910", + "modified": "2023-09-29 17:05:50.502696", "modified_by": "Administrator", "module": "LMS", "name": "User Skill", @@ -44,11 +44,12 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "LMS Student", "select": 1, "share": 1 } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/lms/lms/test_utils.py b/lms/lms/test_utils.py index 3831c5a5..e2ed069d 100644 --- a/lms/lms/test_utils.py +++ b/lms/lms/test_utils.py @@ -72,4 +72,4 @@ def create_evaluation(user, course, date, rating, status): "status": status, } ) - evaluation.save(ignore_permissions=True) + evaluation.save() diff --git a/lms/lms/utils.py b/lms/lms/utils.py index dfefe26f..b3d4e1b9 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -585,7 +585,7 @@ def validate_image(path): if path and "/private" in path: file = frappe.get_doc("File", {"file_url": path}) file.is_private = 0 - file.save(ignore_permissions=True) + file.save() return file.file_url return path diff --git a/lms/patches.txt b/lms/patches.txt index ef457315..40a610ca 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -69,4 +69,5 @@ lms.patches.v1_0.rename_lms_class_to_lms_batch lms.patches.v1_0.rename_classes_in_navbar lms.patches.v1_0.publish_batches lms.patches.v1_0.publish_certificates -lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 \ No newline at end of file +lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 +lms.patches.v1_0.create_student_role \ No newline at end of file diff --git a/lms/patches/v1_0/create_student_role.py b/lms/patches/v1_0/create_student_role.py new file mode 100644 index 00000000..ec0f4e0a --- /dev/null +++ b/lms/patches/v1_0/create_student_role.py @@ -0,0 +1,22 @@ +import frappe +from lms.install import create_lms_student_role + + +def execute(): + create_lms_student_role() + + users = frappe.get_all( + "User", filters={"user_type": "Website User", "enabled": 1}, pluck="name" + ) + + for user in users: + filters = { + "parent": user, + "parenttype": "User", + "parentfield": "roles", + "role": "LMS Student", + } + if not frappe.db.exists("Has Role", filters): + doc = frappe.new_doc("Has Role") + doc.update(filters) + doc.save() From 80843ec44b31595c8b3fd0348797da517455ceb4 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 29 Sep 2023 18:59:08 +0530 Subject: [PATCH 032/112] feat: assign LMS Student role to all signups --- lms/overrides/user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/overrides/user.py b/lms/overrides/user.py index a02db99e..bfc1f49b 100644 --- a/lms/overrides/user.py +++ b/lms/overrides/user.py @@ -235,13 +235,15 @@ def sign_up(email, full_name, verify_terms, user_category): user.flags.ignore_permissions = True user.flags.ignore_password_policy = True user.insert() - set_country_from_ip(None, user.name) # set default signup role as per Portal Settings default_role = frappe.db.get_value("Portal Settings", None, "default_role") if default_role: user.add_roles(default_role) + user.add_roles("LMS Student") + set_country_from_ip(None, user.name) + if user.flags.email_sent: return 1, _("Please check your email for verification") else: From 1000a22490ad48dafde83ceea78250b2ab883c20 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 2 Oct 2023 12:52:36 +0530 Subject: [PATCH 033/112] fix: permissions for lms enrollment --- .../lms_enrollment/lms_enrollment.json | 27 ++++++++++++++++++- lms/patches.txt | 3 ++- lms/www/certified_participants/__init__.py | 0 lms/www/classes/__init__.py | 0 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 lms/www/certified_participants/__init__.py create mode 100644 lms/www/classes/__init__.py diff --git a/lms/lms/doctype/lms_enrollment/lms_enrollment.json b/lms/lms/doctype/lms_enrollment/lms_enrollment.json index 2ebf9161..c68465cb 100644 --- a/lms/lms/doctype/lms_enrollment/lms_enrollment.json +++ b/lms/lms/doctype/lms_enrollment/lms_enrollment.json @@ -123,7 +123,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-24 17:52:35.487141", + "modified": "2023-10-02 12:41:25.139734", "modified_by": "Administrator", "module": "LMS", "name": "LMS Enrollment", @@ -140,6 +140,31 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "LMS Student", + "select": 1, + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Moderator", + "select": 1, + "share": 1, + "write": 1 } ], "quick_entry": 1, diff --git a/lms/patches.txt b/lms/patches.txt index ef457315..ad0ce959 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -69,4 +69,5 @@ lms.patches.v1_0.rename_lms_class_to_lms_batch lms.patches.v1_0.rename_classes_in_navbar lms.patches.v1_0.publish_batches lms.patches.v1_0.publish_certificates -lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 \ No newline at end of file +lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 +execute:frappe.permissions.reset_perms("LMS Enrollment") diff --git a/lms/www/certified_participants/__init__.py b/lms/www/certified_participants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/www/classes/__init__.py b/lms/www/classes/__init__.py new file mode 100644 index 00000000..e69de29b From 277c089adcc238421e6fb6ed744112dd58db3d5e Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 3 Oct 2023 13:33:59 +0530 Subject: [PATCH 034/112] feat: audio in lessons --- lms/hooks.py | 1 + lms/plugins.py | 8 ++++--- lms/www/batch/edit.js | 52 ++++++++++++++++++++++++++++++---------- lms/www/batches/batch.py | 4 ++-- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/lms/hooks.py b/lms/hooks.py index ab6ac7b4..a8913578 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -305,6 +305,7 @@ lms_markdown_macro_renderers = { "Video": "lms.plugins.video_renderer", "Assignment": "lms.plugins.assignment_renderer", "Embed": "lms.plugins.embed_renderer", + "Audio": "lms.plugins.audio_renderer", } # page_renderer to manage profile pages diff --git a/lms/plugins.py b/lms/plugins.py index 59dcd3ab..590820f3 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -178,9 +178,11 @@ def embed_renderer(details): def video_renderer(src): - return ( - f"" - ) + return f"" + + +def audio_renderer(src): + return f"" def assignment_renderer(detail): diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index 9b124812..8922626b 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -119,6 +119,16 @@ const parse_string_to_lesson = (type) => { type: "upload", data: { file_url: video, + file_type: "video", + }, + }); + } else if (block.includes("{{ Audio")) { + let audio = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ + type: "upload", + data: { + file_url: audio, + file_type: "audio", }, }); } else if (block.includes("{{ Embed")) { @@ -136,6 +146,7 @@ const parse_string_to_lesson = (type) => { type: "upload", data: { file_url: image, + file_type: "image", }, }); } else if (block.includes("#")) { @@ -184,9 +195,12 @@ const parse_content_to_string = (data, type) => { lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`; } else if (block.type == "upload") { let url = block.data.file_url; - lesson_content += block.data.is_video - ? `{{ Video("${url}") }}\n` - : `![](${url})`; + lesson_content += + block.data.file_type == "video" + ? `{{ Video("${url}") }}\n` + : block.data.file_type == "audio" + ? `{{ Audio("${url}") }}\n` + : `![](${url})`; } else if (block.type == "header") { lesson_content += "#".repeat(block.data.level) + ` ${block.data.text}\n`; @@ -211,8 +225,6 @@ const parse_content_to_string = (data, type) => { }; const save = () => { - console.log(this.instructor_notes_data); - console.log(this.lesson_content_data); validate_mandatory(this.lesson_content_data); let lesson = $("#lesson-title").data("lesson"); frappe.call({ @@ -260,10 +272,22 @@ const validate_mandatory = (lesson_content) => { } }; -const is_video = (url) => { +const get_file_type = (url) => { let video_types = ["mov", "mp4", "mkv"]; let video_extension = url.split(".").pop(); - return video_types.indexOf(video_extension) >= 0; + + if (video_types.indexOf(video_extension) >= 0) { + return "video"; + } + + let audio_types = ["mp3", "wav", "ogg"]; + let audio_extension = url.split(".").pop(); + + if (audio_types.indexOf(audio_extension) >= 0) { + return "audio"; + } + + return "image"; }; class YouTubeVideo { @@ -444,7 +468,7 @@ class Upload { folder: "Home/Attachments", make_attachments_public: true, restrictions: { - allowed_file_types: ["image/*", "video/*"], + allowed_file_types: ["image/*", "video/*", "audio/*"], }, on_success: (file_doc) => { self.file_url = file_doc.file_url; @@ -454,11 +478,15 @@ class Upload { } render_upload(url) { - this.is_video = is_video(url); - if (this.is_video) { - return `
-
- - -
-
-
- - {% if lesson.body %} -
{{ lesson.body }}
- {% endif %} -
-
+
+
+
+ {{ _("Content") }} +
+
+ {{ _("Add your lesson content here") }} +
+
+ +
+
+
+ + {% if lesson.body %} +
{{ lesson.body }}
+ {% endif %} +
+ {% endmacro %} diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 60279848..48c8376e 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -158,7 +158,7 @@ {% if instructor_notes and (is_moderator or instructor or is_evaluator) %}
- {% if assignment.type != "URL" %} + {% if assignment.type not in ["URL", "Text"] %}
{{ _("Submit")}} @@ -100,17 +101,41 @@
- {{ _("Submit")}} + {{ _("Submission")}}
- {{ _("Enter a URL") }} + {% if assignment.type == "URL" %} + {{ _("Enter a {0}").format(assignment.type) }} + {% else %} + {{ _("Enter your response") }} + {% endif %}
- + {% if assignment.type == "URL" %} + + {% else %} +
+ {% if submission.answer %} +
+ {{ submission.answer }} +
+ {% endif %} + {% endif %}
{% endif %} - {% if is_moderator %} + {% if assignment.show_answer and submission %} +
+
+ {{ _("Response by Instructor:") }} +
+
+ {{ assignment.answer }} +
+
+ {% endif %} + + {% if assignment.grade_assignment and is_moderator %}
{{ _("Status") }} diff --git a/lms/www/assignment_submission/assignment_submission.js b/lms/www/assignment_submission/assignment_submission.js index d5147554..f336abc3 100644 --- a/lms/www/assignment_submission/assignment_submission.js +++ b/lms/www/assignment_submission/assignment_submission.js @@ -1,4 +1,10 @@ frappe.ready(() => { + if ($(".assignment-text").length) { + frappe.require("controls.bundle.js", () => { + make_text_editor(); + }); + } + $(".btn-upload").click((e) => { upload_file(e); }); @@ -52,11 +58,19 @@ const save_assignment = (e) => { file = ""; if (data == "URL") { - answer = $("#assignment-url").val(); + answer = $(".assignment-answer").val(); if (!answer) { frappe.throw({ - title: __("No URL"), - message: __("Please enter a URL."), + title: __("No Submission"), + message: __("Please enter a response."), + }); + } + } else if (data == "Text") { + answer = this.text_editor.get_value("assignment_text"); + if (!answer) { + frappe.throw({ + title: __("No Submission"), + message: __("Please enter a response."), }); } } else { @@ -99,3 +113,20 @@ const clear_preview = (e) => { $("#assignment-preview a").attr("href", ""); $("#assignment-preview .btn-close").addClass("hide"); }; + +const make_text_editor = () => { + this.text_editor = new frappe.ui.FieldGroup({ + fields: [ + { + fieldname: "assignment_text", + fieldtype: "Text Editor", + default: $(".assignment-text-data").html(), + }, + ], + body: $(".assignment-text").get(0), + }); + this.text_editor.make(); + $(".assignment-text .form-section:last").removeClass("empty-section"); + $(".assignment-text .frappe-control").removeClass("hide-control"); + $(".assignment-text .form-column").addClass("p-0"); +}; diff --git a/lms/www/assignment_submission/assignment_submission.py b/lms/www/assignment_submission/assignment_submission.py index a026679e..93ecb2f7 100644 --- a/lms/www/assignment_submission/assignment_submission.py +++ b/lms/www/assignment_submission/assignment_submission.py @@ -14,7 +14,10 @@ def get_context(context): assignment = frappe.form_dict["assignment"] context.assignment = frappe.db.get_value( - "LMS Assignment", assignment, ["title", "name", "type", "question"], as_dict=1 + "LMS Assignment", + assignment, + ["title", "name", "type", "question", "show_answer", "answer", "grade_assignment"], + as_dict=1, ) if submission == "new-submission": @@ -34,6 +37,10 @@ def get_context(context): ], as_dict=True, ) + + if not context.submission: + raise frappe.PermissionError(_("Invalid Submission URL")) + if not context.is_moderator and frappe.session.user != context.submission.member: raise frappe.PermissionError(_("You don't have permission to access this page.")) diff --git a/lms/www/assignments/index.html b/lms/www/assignments/index.html index e04bc3f3..52374455 100644 --- a/lms/www/assignments/index.html +++ b/lms/www/assignments/index.html @@ -5,7 +5,7 @@ {% block content %}
-
+
{{ Header() }} {% if assignments | length %} {{ AssignmentList(assignments) }} @@ -32,7 +32,33 @@ {% macro AssignmentList(assignments) %}
-
    +
    +
    +
    +
    +
    + {{ _("Title") }} +
    +
    + {{ _("Type") }} +
    +
    +
    +
    + {% for assignment in assignments %} +
    +
    + + {{ assignment.title }} + +
    + {{ assignment.type }} +
    +
    +
    + {% endfor %} +
    +
{% endmacro %} diff --git a/lms/www/assignments/index.py b/lms/www/assignments/index.py index 528b92f8..f8d9e3a4 100644 --- a/lms/www/assignments/index.py +++ b/lms/www/assignments/index.py @@ -1,10 +1,17 @@ import frappe +from lms.lms.utils import has_course_moderator_role def get_context(context): context.no_cache = 1 + + filters = {"owner": frappe.session.user} + + if has_course_moderator_role(): + filters = {} + context.assignments = frappe.get_all( "LMS Assignment", - {"owner": frappe.session.user}, + filters, ["title", "name", "type", "question"], ) From 814870fd698490306b5a9411983923602e1086a0 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 9 Oct 2023 18:53:50 +0530 Subject: [PATCH 043/112] feat: batch regisration confirmation email --- .../doctype/batch_student/batch_student.json | 9 +++- lms/lms/doctype/lms_batch/lms_batch.py | 48 ++++++++++++++++++- .../doctype/lms_settings/lms_settings.json | 12 ++++- lms/templates/emails/batch_confirmation.html | 38 +++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 lms/templates/emails/batch_confirmation.html diff --git a/lms/lms/doctype/batch_student/batch_student.json b/lms/lms/doctype/batch_student/batch_student.json index 699050df..313c79c4 100644 --- a/lms/lms/doctype/batch_student/batch_student.json +++ b/lms/lms/doctype/batch_student/batch_student.json @@ -10,6 +10,7 @@ "student_details_section", "student", "payment", + "confirmation_email_sent", "column_break_oduu", "student_name", "username" @@ -52,12 +53,18 @@ "fieldtype": "Link", "label": "Payment", "options": "LMS Payment" + }, + { + "default": "0", + "fieldname": "confirmation_email_sent", + "fieldtype": "Check", + "label": "Confirmation Email Sent" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-09-12 16:46:41.042810", + "modified": "2023-10-09 17:09:50.481794", "modified_by": "Administrator", "module": "LMS", "name": "Batch Student", diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index b21f24ab..68ac25a7 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -8,9 +8,17 @@ import json from frappe import _ from datetime import timedelta from frappe.model.document import Document -from frappe.utils import cint, format_date, format_datetime +from frappe.utils import ( + cint, + format_date, + format_datetime, + add_to_date, + getdate, + get_datetime, +) from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url from lms.www.utils import get_quiz_details, get_assignment_details +from frappe.email.doctype.email_template.email_template import get_email_template class LMSBatch(Document): @@ -22,6 +30,7 @@ class LMSBatch(Document): self.validate_duplicate_assessments() self.validate_membership() self.validate_timetable() + self.send_confirmation_mail() def validate_duplicate_students(self): students = [row.student for row in self.students] @@ -55,6 +64,43 @@ class LMSBatch(Document): ) ) + def send_confirmation_mail(self): + for student in self.students: + + if not student.confirmation_email_sent: + self.send_mail(student) + student.confirmation_email_sent = 1 + + def send_mail(self, student): + subject = _("Enrollment Confirmation for the Next Training Batch") + template = "batch_confirmation" + custom_template = frappe.db.get_single_value( + "LMS Settings", "batch_confirmation_template" + ) + + args = { + "student_name": student.student_name, + "start_time": self.start_time, + "start_date": self.start_date, + "medium": self.medium, + "name": self.name, + } + + if custom_template: + email_template = get_email_template(custom_template, args) + subject = email_template.get("subject") + content = email_template.get("message") + + frappe.sendmail( + recipients=student.student, + subject=subject, + template=template if not custom_template else None, + content=content if custom_template else None, + args=args, + header=[subject, "green"], + retry=3, + ) + def validate_membership(self): for course in self.courses: for student in self.students: diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index 36741a4b..13b98a9f 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -16,6 +16,7 @@ "portal_course_creation", "section_break_szgq", "send_calendar_invite_for_evaluations", + "batch_confirmation_template", "column_break_2", "allow_student_progress", "payment_section", @@ -176,7 +177,7 @@ { "fieldname": "section_break_szgq", "fieldtype": "Section Break", - "label": "Class Settings" + "label": "Batch Settings" }, { "fieldname": "signup_settings_tab", @@ -186,6 +187,7 @@ { "fieldname": "mentor_request_tab", "fieldtype": "Tab Break", + "hidden": 1, "label": "Mentor Request" }, { @@ -253,12 +255,18 @@ "fieldname": "apply_rounding", "fieldtype": "Check", "label": "Apply Rounding on Equivalent" + }, + { + "fieldname": "batch_confirmation_template", + "fieldtype": "Link", + "label": "Batch Confirmation Template", + "options": "Email Template" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-11 21:56:39.996898", + "modified": "2023-10-09 17:27:28.615355", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", diff --git a/lms/templates/emails/batch_confirmation.html b/lms/templates/emails/batch_confirmation.html new file mode 100644 index 00000000..4e9ad30a --- /dev/null +++ b/lms/templates/emails/batch_confirmation.html @@ -0,0 +1,38 @@ +

+ {{ _("Dear ") }} {{ student_name }}, +

+
+

+ {{ _("I am pleased to inform you that your enrollment for the upcoming training batch has been successfully processed. Congratulations!") }} +

+
+

+ + {{ _("Important Details:") }} + +

+ +

+ {{ _("Batch Start Date:") }} {{ frappe.utils.format_date(start_date, "medium") }} +

+ +

+ {{ _("Medium:") }} {{ medium }} +

+ +

+ {{ _("Timings:") }} {{ frappe.utils.format_time(start_time, "hh:mm a") }} +

+
+

+ {{ _("Visit the following link to view your ") }} + {{ _("Batch Details") }} +

+

+ {{ _("If you have any questions or require assistance, feel free to contact us.") }} +

+
+

+ {{ _("Best Regards") }} +

+ From 1046d28092508fc9d4300729f762191369dd7676 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 11 Oct 2023 12:25:46 +0530 Subject: [PATCH 044/112] 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 a0255e174370b27590277e180571c89029aefe70 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 11 Oct 2023 12:58:07 +0530 Subject: [PATCH 045/112] feat: send email to batch students --- .../doctype/lms_assignment/lms_assignment.py | 4 ++-- lms/lms/doctype/lms_course/lms_course.py | 2 +- lms/lms/doctype/lms_quiz/lms_quiz.py | 8 +++++-- lms/lms/utils.py | 22 +++++++++++++++---- lms/www/assignments/assignment.py | 4 ++-- lms/www/batch/quiz.py | 4 ++-- lms/www/batch/quiz_list.py | 4 ++-- lms/www/courses/course.py | 2 +- lms/www/courses/create.py | 2 +- lms/www/courses/index.py | 7 ++++-- 10 files changed, 40 insertions(+), 19 deletions(-) diff --git a/lms/lms/doctype/lms_assignment/lms_assignment.py b/lms/lms/doctype/lms_assignment/lms_assignment.py index 647833de..b4aaedbf 100644 --- a/lms/lms/doctype/lms_assignment/lms_assignment.py +++ b/lms/lms/doctype/lms_assignment/lms_assignment.py @@ -3,7 +3,7 @@ import frappe from frappe.model.document import Document -from lms.lms.utils import can_create_courses +from lms.lms.utils import has_course_moderator_role, has_course_instructor_role class LMSAssignment(Document): @@ -12,7 +12,7 @@ class LMSAssignment(Document): @frappe.whitelist() def save_assignment(assignment, title, type, question): - if not can_create_courses(): + if not has_course_moderator_role() or not has_course_instructor_role(): return if assignment: diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 74ccc51a..9065b639 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -216,7 +216,7 @@ def save_course( course_price=None, currency=None, ): - if not can_create_courses(): + if not can_create_courses(course): return if course: diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 9cb5789c..e6794843 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -6,7 +6,11 @@ import frappe 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.utils import ( + generate_slug, + has_course_moderator_role, + has_course_instructor_role, +) class LMSQuiz(Document): @@ -148,7 +152,7 @@ def quiz_summary(quiz, results): def save_quiz( quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0 ): - if not can_create_courses(): + if not has_course_moderator_role() or not has_course_instructor_role(): return values = { diff --git a/lms/lms/utils.py b/lms/lms/utils.py index b3d4e1b9..f7dbe489 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -521,21 +521,35 @@ def has_course_instructor_role(member=None): ) -def can_create_courses(member=None): +def can_create_courses(course, member=None): if not member: member = frappe.session.user + instructors = frappe.get_all( + "Course Instructor", + { + "parent": course, + }, + pluck="instructor", + ) + if frappe.session.user == "Guest": return False - if has_course_instructor_role(member) or has_course_moderator_role(member): + if has_course_moderator_role(member): + return True + + if has_course_instructor_role(member) and member in instructors: return True portal_course_creation = frappe.db.get_single_value( "LMS Settings", "portal_course_creation" ) - return portal_course_creation == "Anyone" + if portal_course_creation == "Anyone" and member in instructors: + return True + + return False def has_course_moderator_role(member=None): @@ -727,7 +741,7 @@ def get_chart_data(chart_name, timespan, timegrain, from_date, to_date): } -@frappe.whitelist() +@frappe.whitelist(allow_guest=True) def get_course_completion_data(): all_membership = frappe.db.count("LMS Enrollment") completed = frappe.db.count("LMS Enrollment", {"progress": ["like", "%100%"]}) diff --git a/lms/www/assignments/assignment.py b/lms/www/assignments/assignment.py index 9b1c299d..ec8cc543 100644 --- a/lms/www/assignments/assignment.py +++ b/lms/www/assignments/assignment.py @@ -1,12 +1,12 @@ import frappe from frappe import _ -from lms.lms.utils import can_create_courses +from lms.lms.utils import has_course_moderator_role, has_course_instructor_role def get_context(context): context.no_cache = 1 - if not can_create_courses(): + if not has_course_moderator_role() or not has_course_instructor_role(): message = "You do not have permission to access this page." if frappe.session.user == "Guest": message = "Please login to access this page." diff --git a/lms/www/batch/quiz.py b/lms/www/batch/quiz.py index f88aaef5..52d0634f 100644 --- a/lms/www/batch/quiz.py +++ b/lms/www/batch/quiz.py @@ -1,13 +1,13 @@ import frappe from frappe.utils import cstr from frappe import _ -from lms.lms.utils import can_create_courses +from lms.lms.utils import has_course_instructor_role, has_course_moderator_role def get_context(context): context.no_cache = 1 - if not can_create_courses(): + if not has_course_moderator_role() or not has_course_instructor_role(): message = "You do not have permission to access this page." if frappe.session.user == "Guest": message = "Please login to access this page." diff --git a/lms/www/batch/quiz_list.py b/lms/www/batch/quiz_list.py index ae8df7fc..ee4321a7 100644 --- a/lms/www/batch/quiz_list.py +++ b/lms/www/batch/quiz_list.py @@ -1,12 +1,12 @@ import frappe -from lms.lms.utils import can_create_courses, has_course_moderator_role +from lms.lms.utils import has_course_instructor_role, has_course_moderator_role from frappe import _ def get_context(context): context.no_cache = 1 - if not can_create_courses(): + if not has_course_moderator_role() or not has_course_instructor_role(): message = "You do not have permission to access this page." if frappe.session.user == "Guest": message = "Please login to access this page." diff --git a/lms/www/courses/course.py b/lms/www/courses/course.py index b4aa476c..b64395eb 100644 --- a/lms/www/courses/course.py +++ b/lms/www/courses/course.py @@ -23,7 +23,7 @@ def get_context(context): redirect_to_courses_list() if course_name == "new-course": - if not can_create_courses(): + if not can_create_courses(course_name): message = "You do not have permission to access this page." if frappe.session.user == "Guest": message = "Please login to access this page." diff --git a/lms/www/courses/create.py b/lms/www/courses/create.py index 901ce1f2..7b83f3f5 100644 --- a/lms/www/courses/create.py +++ b/lms/www/courses/create.py @@ -15,7 +15,7 @@ def get_context(context): except KeyError: redirect_to_courses_list() - if not can_create_courses(): + if not can_create_courses(course_name): message = "You do not have permission to access this page." if frappe.session.user == "Guest": message = "Please login to access this page." diff --git a/lms/www/courses/index.py b/lms/www/courses/index.py index d887e1ca..62474be0 100644 --- a/lms/www/courses/index.py +++ b/lms/www/courses/index.py @@ -1,7 +1,6 @@ import frappe from frappe import _ from lms.lms.utils import ( - can_create_courses, check_profile_restriction, get_restriction_details, has_course_moderator_role, @@ -21,7 +20,11 @@ def get_context(context): context.created_courses = get_authored_courses(None, False) context.review_courses = get_courses_under_review() context.restriction = check_profile_restriction() - context.show_creators_section = can_create_courses() + + portal_course_creation = frappe.db.get_single_value( + "LMS Settings", "portal_course_creation" + ) + context.show_creators_section = True if portal_course_creation == "Anyone" else False context.show_review_section = ( has_course_moderator_role() and frappe.session.user != "Guest" ) From f3d6ad6c849fa20f1c9a1d9f34274b7a5e3495c5 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 11 Oct 2023 13:40:07 +0530 Subject: [PATCH 046/112] fix: course permissions --- lms/www/courses/index.py | 9 ++++++++- lms/www/courses/outline.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lms/www/courses/index.py b/lms/www/courses/index.py index 62474be0..7df2a960 100644 --- a/lms/www/courses/index.py +++ b/lms/www/courses/index.py @@ -7,6 +7,7 @@ from lms.lms.utils import ( get_courses_under_review, get_average_rating, check_multicurrency, + has_course_instructor_role, ) from lms.overrides.user import get_enrolled_courses, get_authored_courses @@ -24,7 +25,13 @@ def get_context(context): portal_course_creation = frappe.db.get_single_value( "LMS Settings", "portal_course_creation" ) - context.show_creators_section = True if portal_course_creation == "Anyone" else False + context.show_creators_section = ( + True + if portal_course_creation == "Anyone" + or has_course_moderator_role() + or has_course_instructor_role() + else False + ) context.show_review_section = ( has_course_moderator_role() and frappe.session.user != "Guest" ) diff --git a/lms/www/courses/outline.py b/lms/www/courses/outline.py index ad8f18d9..f01a71af 100644 --- a/lms/www/courses/outline.py +++ b/lms/www/courses/outline.py @@ -10,7 +10,7 @@ def get_context(context): if not frappe.db.exists("LMS Course", course_name): redirect_to_courses_list() - if not can_create_courses(): + if not can_create_courses(course_name): message = "You do not have permission to access this page." if frappe.session.user == "Guest": message = "Please login to access this page." From a7dbdd844b2a9d2e13752edb1c90f211b95bd8c3 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 12 Oct 2023 21:20:36 +0530 Subject: [PATCH 047/112] feat: batch customisations --- .github/workflows/ci.yml | 2 +- lms/lms/doctype/lms_batch/lms_batch.js | 110 ++++++++++++------ lms/lms/doctype/lms_batch/lms_batch.json | 18 ++- .../doctype/lms_timetable_legend/__init__.py | 0 .../lms_timetable_legend.js | 8 ++ .../lms_timetable_legend.json | 52 +++++++++ .../lms_timetable_legend.py | 9 ++ .../test_lms_timetable_legend.py | 9 ++ .../lms_timetable_template.js | 14 +++ .../lms_timetable_template.json | 11 +- lms/www/batches/batch.html | 4 +- lms/www/batches/batch.js | 30 +++-- lms/www/batches/batch.py | 28 ++--- 13 files changed, 226 insertions(+), 69 deletions(-) create mode 100644 lms/lms/doctype/lms_timetable_legend/__init__.py create mode 100644 lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.js create mode 100644 lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json create mode 100644 lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.py create mode 100644 lms/lms/doctype/lms_timetable_legend/test_lms_timetable_legend.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aafbfa01..1c0c28c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: ports: - 12000:6379 mariadb: - image: anandology/mariadb-utf8mb4:10.3 + image: mariadb:10.6 ports: - 3306:3306 env: diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index f7273fc1..1a95c493 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -12,12 +12,16 @@ frappe.ui.form.on("LMS Batch", { }); frm.set_query("reference_doctype", "timetable", function () { - let doctypes = [ - "Course Lesson", - "LMS Quiz", - "LMS Assignment", - "LMS Live Class", - ]; + let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"]; + return { + filters: { + name: ["in", doctypes], + }, + }; + }); + + frm.set_query("reference_doctype", "timetable_legends", function () { + let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"]; return { filters: { name: ["in", doctypes], @@ -27,36 +31,41 @@ frappe.ui.form.on("LMS Batch", { }, timetable_template: function (frm) { - if (frm.doc.timetable_template) { - frm.clear_table("timetable"); - frm.refresh_fields(); - - frappe.call({ - method: "frappe.client.get_list", - args: { - doctype: "LMS Batch Timetable", - parent: "LMS Timetable Template", - fields: [ - "reference_doctype", - "reference_docname", - "day", - "start_time", - "end_time", - "duration", - ], - filters: { - parent: frm.doc.timetable_template, - }, - order_by: "idx", - }, - callback: (data) => { - add_timetable_rows(frm, data.message); - }, - }); - } + set_timetable(frm); }, }); +const set_timetable = (frm) => { + if (frm.doc.timetable_template) { + frm.clear_table("timetable"); + frm.refresh_fields(); + + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "LMS Batch Timetable", + parent: "LMS Timetable Template", + fields: [ + "reference_doctype", + "reference_docname", + "day", + "start_time", + "end_time", + "duration", + ], + filters: { + parent: frm.doc.timetable_template, + parenttype: "LMS Timetable Template", + }, + order_by: "idx", + }, + callback: (data) => { + add_timetable_rows(frm, data.message); + }, + }); + } +}; + const add_timetable_rows = (frm, timetable) => { timetable.forEach((row) => { let child = frm.add_child("timetable"); @@ -75,5 +84,40 @@ const add_timetable_rows = (frm, timetable) => { child.duration = row.duration; }); frm.refresh_field("timetable"); + + set_legends(frm); +}; + +const set_legends = (frm) => { + if (frm.doc.timetable_template) { + frm.clear_table("timetable_legends"); + frm.refresh_fields(); + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "LMS Timetable Legend", + parent: "LMS Timetable Template", + fields: ["reference_doctype", "label", "color"], + filters: { + parent: frm.doc.timetable_template, + parenttype: "LMS Timetable Template", + }, + order_by: "idx", + }, + callback: (data) => { + add_legend_rows(frm, data.message); + }, + }); + } +}; + +const add_legend_rows = (frm, legends) => { + legends.forEach((row) => { + let child = frm.add_child("timetable_legends"); + child.reference_doctype = row.reference_doctype; + child.label = row.label; + child.color = row.color; + }); + frm.refresh_field("timetable_legends"); frm.save(); }; diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index 4cdb1e0a..f0838a4b 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -35,8 +35,10 @@ "timetable_template", "column_break_anya", "show_live_class", + "allow_future", "section_break_ontp", "timetable", + "timetable_legends", "pricing_tab", "section_break_gsac", "paid_batch", @@ -220,7 +222,7 @@ "default": "0", "fieldname": "show_live_class", "fieldtype": "Check", - "label": "Show Live Class" + "label": "Show live class" }, { "fieldname": "section_break_ontp", @@ -263,11 +265,23 @@ "fieldtype": "Code", "label": "Custom Script (JavaScript)", "options": "Javascript" + }, + { + "fieldname": "timetable_legends", + "fieldtype": "Table", + "label": "Timetable Legends", + "options": "LMS Timetable Legend" + }, + { + "default": "1", + "fieldname": "allow_future", + "fieldtype": "Check", + "label": "Allow accessing future dates" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-28 12:18:34.418812", + "modified": "2023-10-12 12:53:37.351989", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch", diff --git a/lms/lms/doctype/lms_timetable_legend/__init__.py b/lms/lms/doctype/lms_timetable_legend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.js b/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.js new file mode 100644 index 00000000..7a8a3684 --- /dev/null +++ b/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("LMS Timetable Legend", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json b/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json new file mode 100644 index 00000000..6ae0ffe4 --- /dev/null +++ b/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2023-10-11 16:36:45.079267", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "label", + "color" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "color", + "fieldtype": "Color", + "in_list_view": 1, + "label": "Color", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-10-11 17:15:37.039139", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Timetable Legend", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.py b/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.py new file mode 100644 index 00000000..c842e3a2 --- /dev/null +++ b/lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LMSTimetableLegend(Document): + pass diff --git a/lms/lms/doctype/lms_timetable_legend/test_lms_timetable_legend.py b/lms/lms/doctype/lms_timetable_legend/test_lms_timetable_legend.py new file mode 100644 index 00000000..816b1793 --- /dev/null +++ b/lms/lms/doctype/lms_timetable_legend/test_lms_timetable_legend.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSTimetableLegend(FrappeTestCase): + pass diff --git a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js index a9bff04e..b5ec2d9b 100644 --- a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js @@ -11,5 +11,19 @@ frappe.ui.form.on("LMS Timetable Template", { }, }; }); + + frm.set_query("reference_doctype", "timetable_legends", function () { + let doctypes = [ + "Course Lesson", + "LMS Quiz", + "LMS Assignment", + "LMS Live Class", + ]; + return { + filters: { + name: ["in", doctypes], + }, + }; + }); }, }); diff --git a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json index 99c88628..3b016b7a 100644 --- a/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json @@ -8,7 +8,8 @@ "engine": "InnoDB", "field_order": [ "title", - "timetable" + "timetable", + "timetable_legends" ], "fields": [ { @@ -21,11 +22,17 @@ "fieldtype": "Table", "label": "Timetable", "options": "LMS Batch Timetable" + }, + { + "fieldname": "timetable_legends", + "fieldtype": "Table", + "label": "Timetable Legends", + "options": "LMS Timetable Legend" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-18 17:57:15.819072", + "modified": "2023-10-11 17:09:05.096243", "modified_by": "Administrator", "module": "LMS", "name": "LMS Timetable Template", diff --git a/lms/www/batches/batch.html b/lms/www/batches/batch.html index 2f78e0c6..86cb1202 100644 --- a/lms/www/batches/batch.html +++ b/lms/www/batches/batch.html @@ -552,7 +552,7 @@ {% for legend in legends %}
-
{{ legend.title }}
+
{{ legend.label }}
{% endfor %}
@@ -574,6 +574,8 @@ diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index 1f4cd60d..6fb088a8 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -653,7 +653,6 @@ 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); @@ -698,8 +697,8 @@ const get_calendar_options = (element, calendar_id) => { const create_events = (calendar, events, calendar_id) => { let calendar_events = []; - events.forEach((event, idx) => { + let clr = get_background_color(event.reference_doctype); calendar_events.push({ id: `event${idx}`, calendarId: calendar_id, @@ -707,7 +706,7 @@ const create_events = (calendar, events, calendar_id) => { start: `${event.date}T${event.start_time}`, end: `${event.date}T${event.end_time}`, isAllday: event.start_time ? false : true, - borderColor: get_background_color(event.reference_doctype), + borderColor: clr, backgroundColor: "var(--fg-color)", customStyle: { borderRadius: "var(--border-radius-md)", @@ -724,10 +723,21 @@ const create_events = (calendar, events, calendar_id) => { calendar.createEvents(calendar_events); }; -const add_links_to_events = (calendar, events) => { +const add_links_to_events = (calendar) => { calendar.on("clickEvent", ({ event }) => { - const el = document.getElementById("clicked-event"); - window.open(event.raw.url, "_blank"); + let event_date = event.start.d.d; + event_date = moment(event_date).format("YYYY-MM-DD"); + + let current_date = moment().format("YYYY-MM-DD"); + console.log(current_date, event_date); + console.log( + allow_future, + moment(event_date).isSameOrBefore(current_date) + ); + if (allow_future || moment(event_date).isSameOrBefore(current_date)) { + console.log("in here"); + window.open(event.raw.url, "_blank"); + } }); }; @@ -764,10 +774,10 @@ const set_calendar_range = (calendar, events) => { }; const get_background_color = (doctype) => { - if (doctype == "Course Lesson") return "var(--blue-400)"; - if (doctype == "LMS Quiz") return "var(--green-400)"; - if (doctype == "LMS Assignment") return "var(--orange-400)"; - if (doctype == "LMS Live Class") return "var(--purple-400)"; + const match = legends.filter((legend) => { + return legend.reference_doctype == doctype; + }); + if (match.length) return match[0].color; }; const email_to_students = () => { diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index 5e408c84..bb3ad307 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -42,6 +42,7 @@ def get_context(context): "currency", "batch_details", "published", + "allow_future", ], as_dict=True, ) @@ -96,7 +97,7 @@ def get_context(context): "parent": batch_name, }, ) - context.legends = get_legends() + context.legends = get_legends(batch_name) custom_tabs = frappe.get_hooks("lms_batch_tabs") @@ -261,22 +262,9 @@ def get_course_progress(batch_courses, student_details): student_details.courses[course.course] = 0 -def get_legends(): - return [ - { - "title": "Lesson", - "color": "var(--blue-400)", - }, - { - "title": "Quiz", - "color": "var(--green-400)", - }, - { - "title": "Assignment", - "color": "var(--orange-400)", - }, - { - "title": "Live Class", - "color": "var(--purple-400)", - }, - ] +def get_legends(batch): + return frappe.get_all( + "LMS Timetable Legend", + filters={"parenttype": "LMS Batch", "parent": batch}, + fields=["reference_doctype", "color", "label"], + ) From 55feb419985258da8a253bcb0e4085bec9fe0467 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 10:59:44 +0530 Subject: [PATCH 048/112] feat: timetable customisations --- lms/www/batches/batch.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index 6fb088a8..9614a924 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -729,13 +729,7 @@ const add_links_to_events = (calendar) => { event_date = moment(event_date).format("YYYY-MM-DD"); let current_date = moment().format("YYYY-MM-DD"); - console.log(current_date, event_date); - console.log( - allow_future, - moment(event_date).isSameOrBefore(current_date) - ); if (allow_future || moment(event_date).isSameOrBefore(current_date)) { - console.log("in here"); window.open(event.raw.url, "_blank"); } }); From 1e458921e8af81e2ca114e70f11fbccca6cb5878 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 11:17:41 +0530 Subject: [PATCH 049/112] ci: fix server tests script --- .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c0c28c7..a4ad07e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,26 +28,32 @@ jobs: MYSQL_ROOT_PASSWORD: root options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: + - name: Clone - uses: actions/checkout@v2 - - name: setup python + + - name: Setup Python uses: actions/setup-python@v2 with: python-version: '3.10' - - name: setup node + + - name: Setup Node uses: actions/setup-node@v2 with: node-version: '18' check-latest: true - - name: setup cache for bench + + - name: Setup ache for bench uses: actions/cache@v2 with: path: ~/bench-cache key: ${{ runner.os }} - - name: install bench + + - name: Install Bench run: | pip3 install frappe-bench which bench - - name: bench init + + - name: Bench Init run: | if [ -d ~/bench-cache/bench.tgz ] then @@ -57,25 +63,32 @@ jobs: mkdir -p ~/bench-cache (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) fi - - name: add lms app to bench + + - name: Add LMS app to bench working-directory: /home/runner/frappe-bench run: bench get-app lms $GITHUB_WORKSPACE - - name: create bench site + + - name: Create bench site working-directory: /home/runner/frappe-bench - run: bench new-site --mariadb-root-password root --admin-password admin frappe.local - - name: install lms app + run: bench new-site --mariadb-root-password root --character-set-server utf8mb4 --collation-server utf8mb4_unicode_ci --admin-password admin frappe.local + + - name: Install LMS app working-directory: /home/runner/frappe-bench run: bench --site frappe.local install-app lms - - name: setup requirements + + - name: Setup Requirements working-directory: /home/runner/frappe-bench run: bench setup requirements --dev - - name: allow tests + + - name: Allow Tests working-directory: /home/runner/frappe-bench run: bench --site frappe.local set-config allow_tests true - - name: bench build + + - name: Build working-directory: /home/runner/frappe-bench run: bench --site frappe.local build - - name: run tests + + - name: Run Tests working-directory: /home/runner/frappe-bench run: bench --site frappe.local run-tests --app lms From d840d2fc18e021a097d7417151aa14e426475e76 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 11:19:27 +0530 Subject: [PATCH 050/112] ci: fixed step in server tests script --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4ad07e9..439c6613 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Clone - - uses: actions/checkout@v2 + uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 From bf5cc5e1d167ba76c3cf981e92b19fe4dd3f9176 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 11:24:58 +0530 Subject: [PATCH 051/112] ci: fixed mariadb options --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 439c6613..dc73014d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - 3306:3306 env: MYSQL_ROOT_PASSWORD: root - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 --character-set-server utf8mb4 --collation-server utf8mb4_unicode_ci steps: - name: Clone uses: actions/checkout@v2 @@ -70,7 +70,7 @@ jobs: - name: Create bench site working-directory: /home/runner/frappe-bench - run: bench new-site --mariadb-root-password root --character-set-server utf8mb4 --collation-server utf8mb4_unicode_ci --admin-password admin frappe.local + run: bench new-site --mariadb-root-password root --admin-password admin frappe.local - name: Install LMS app working-directory: /home/runner/frappe-bench From a1bb7962bcd3186d2ec88e4a3e8300d70f899368 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 11:40:17 +0530 Subject: [PATCH 052/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc73014d..9a1061a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,11 @@ jobs: image: mariadb:10.6 ports: - 3306:3306 + with: + collation server: 'utf8mb4_unicode_ci' env: MYSQL_ROOT_PASSWORD: root - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 --character-set-server utf8mb4 --collation-server utf8mb4_unicode_ci + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Clone uses: actions/checkout@v2 From 3f5c3e89c8784404ddc4cfa047629d9748016587 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 11:45:42 +0530 Subject: [PATCH 053/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a1061a1..e02c1ba8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,7 @@ jobs: image: mariadb:10.6 ports: - 3306:3306 - with: - collation server: 'utf8mb4_unicode_ci' + collation server: 'utf8mb4_unicode_ci' env: MYSQL_ROOT_PASSWORD: root options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 From 2ff3d83d8f84ca08f099b56d5609a59e43c3ab18 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 11:52:38 +0530 Subject: [PATCH 054/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e02c1ba8..03f6e315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: image: mariadb:10.6 ports: - 3306:3306 - collation server: 'utf8mb4_unicode_ci' + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci env: MYSQL_ROOT_PASSWORD: root options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 From bf0cb25a8804a0085e8b1f368e578c356d575f0a Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 12:33:16 +0530 Subject: [PATCH 055/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03f6e315..7ee6f555 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,18 +20,20 @@ jobs: image: redis:alpine ports: - 12000:6379 - mariadb: - image: mariadb:10.6 - ports: - - 3306:3306 - command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - env: - MYSQL_ROOT_PASSWORD: root - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + steps: - name: Clone uses: actions/checkout@v2 + - name: Start MariaDB + uses: getong/mariadb-action@v1.1 + host port: 3306 + container port: 3306 + character set server: 'utf8mb4' + collation server: 'utf8_general_ci' + mariadb version: '10.6' + mysql root password: root + - name: Setup Python uses: actions/setup-python@v2 with: From f592cf08d88f08fda93ea6ef51e30e03ebbafa02 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 12:47:36 +0530 Subject: [PATCH 056/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 159 +++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 81 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ee6f555..5a485f16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,97 +1,94 @@ name: Server Tests on: - push: - branches: - - main - pull_request: {} + push: + branches: + - main + pull_request: {} jobs: - tests: - runs-on: ubuntu-latest - services: - redis-cache: - image: redis:alpine - ports: - - 13000:6379 - redis-queue: - image: redis:alpine - ports: - - 11000:6379 - redis-socketio: - image: redis:alpine - ports: - - 12000:6379 - - steps: - - name: Clone - uses: actions/checkout@v2 + tests: + runs-on: ubuntu-latest + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + redis-socketio: + image: redis:alpine + ports: + - 12000:6379 + mysql: + image: mariadb:10.6 + env: + MARIADB_ROOT_PASSWORD: "root" + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - - name: Start MariaDB - uses: getong/mariadb-action@v1.1 - host port: 3306 - container port: 3306 - character set server: 'utf8mb4' - collation server: 'utf8_general_ci' - mariadb version: '10.6' - mysql root password: root + steps: + - name: Clone + uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: '3.10' + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" - - name: Setup Node - uses: actions/setup-node@v2 - with: - node-version: '18' - check-latest: true + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 18 + check-latest: true - - name: Setup ache for bench - uses: actions/cache@v2 - with: - path: ~/bench-cache - key: ${{ runner.os }} + - name: Setup ache for bench + uses: actions/cache@v2 + with: + path: ~/bench-cache + key: ${{ runner.os }} - - name: Install Bench - run: | - pip3 install frappe-bench - which bench + - name: Install Bench + run: | + pip3 install frappe-bench + which bench - - name: Bench Init - run: | - if [ -d ~/bench-cache/bench.tgz ] - then - (cd && tar xzf ~/bench-cache/bench.tgz) - else - bench init ~/frappe-bench --skip-redis-config-generation --skip-assets --python "$(which python)" - mkdir -p ~/bench-cache - (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) - fi + - name: Bench Init + run: | + if [ -d ~/bench-cache/bench.tgz ] + then + (cd && tar xzf ~/bench-cache/bench.tgz) + else + bench init ~/frappe-bench --skip-redis-config-generation --skip-assets --python "$(which python)" + mkdir -p ~/bench-cache + (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) + fi - - name: Add LMS app to bench - working-directory: /home/runner/frappe-bench - run: bench get-app lms $GITHUB_WORKSPACE + - name: Add LMS app to bench + working-directory: /home/runner/frappe-bench + run: bench get-app lms $GITHUB_WORKSPACE - - name: Create bench site - working-directory: /home/runner/frappe-bench - run: bench new-site --mariadb-root-password root --admin-password admin frappe.local + - name: Create bench site + working-directory: /home/runner/frappe-bench + run: bench new-site --mariadb-root-password root --admin-password admin frappe.local - - name: Install LMS app - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local install-app lms + - name: Install LMS app + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local install-app lms - - name: Setup Requirements - working-directory: /home/runner/frappe-bench - run: bench setup requirements --dev + - name: Setup Requirements + working-directory: /home/runner/frappe-bench + run: bench setup requirements --dev - - name: Allow Tests - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local set-config allow_tests true + - name: Allow Tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local set-config allow_tests true - - name: Build - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local build - - - name: Run Tests - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local run-tests --app lms + - name: Build + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local build + - name: Run Tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local run-tests --app lms From 4c83264c4a4d0869daa649a16428f261a733e6af Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 13:14:35 +0530 Subject: [PATCH 057/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a485f16..6dd16f80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,11 @@ jobs: image: redis:alpine ports: - 12000:6379 - mysql: + mariadb: image: mariadb:10.6 env: - MARIADB_ROOT_PASSWORD: "root" + MYSQL_ROOT_PASSWORD: "root" + COLLATION_SERVER: "utf8mb4_unicode_ci" ports: - 3306:3306 options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 From c20fa7e093869adcd4b43ff4629ab429f9f0c3dc Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 14:50:50 +0530 Subject: [PATCH 058/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 102 +++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dd16f80..ee4a2cd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,65 +31,71 @@ jobs: steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" + + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -q -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: 18 check-latest: true - - name: Setup ache for bench - uses: actions/cache@v2 + - name: Add to Hosts + run: | + echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v3 with: - path: ~/bench-cache - key: ${{ runner.os }} + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- - - name: Install Bench + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies run: | - pip3 install frappe-bench - which bench - - - name: Bench Init - run: | - if [ -d ~/bench-cache/bench.tgz ] - then - (cd && tar xzf ~/bench-cache/bench.tgz) - else - bench init ~/frappe-bench --skip-redis-config-generation --skip-assets --python "$(which python)" - mkdir -p ~/bench-cache - (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) - fi - - - name: Add LMS app to bench - working-directory: /home/runner/frappe-bench - run: bench get-app lms $GITHUB_WORKSPACE - - - name: Create bench site - working-directory: /home/runner/frappe-bench - run: bench new-site --mariadb-root-password root --admin-password admin frappe.local - - - name: Install LMS app - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local install-app lms - - - name: Setup Requirements - working-directory: /home/runner/frappe-bench - run: bench setup requirements --dev - - - name: Allow Tests - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local set-config allow_tests true - - - name: Build - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local build + bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: server + DB: ${{ matrix.db }} - name: Run Tests - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local run-tests --app lms + run: cd ~/frappe-bench/ && bench --site lms.test run-parallel-tests --app lms --total-builds 4 --build-number ${{ matrix.container }} + env: + SITE: lms.test + CI_BUILD_ID: ${{ github.run_id }} + BUILD_NUMBER: ${{ matrix.container }} + TOTAL_BUILDS: 2 + + - name: Show bench output + if: ${{ always() }} + run: cat ~/frappe-bench/bench_start.log || true + From 12c5ad54e722e1976ad03c4066b13831690fa364 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 15:24:12 +0530 Subject: [PATCH 059/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 168 +++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 86 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee4a2cd7..7ae67c73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,101 +1,97 @@ name: Server Tests + on: - push: - branches: - - main - pull_request: {} + push: + branches: + - main + pull_request: {} + jobs: - tests: - runs-on: ubuntu-latest - services: - redis-cache: - image: redis:alpine - ports: - - 13000:6379 - redis-queue: - image: redis:alpine - ports: - - 11000:6379 - redis-socketio: - image: redis:alpine - ports: - - 12000:6379 - mariadb: - image: mariadb:10.6 - env: - MYSQL_ROOT_PASSWORD: "root" - COLLATION_SERVER: "utf8mb4_unicode_ci" - ports: - - 3306:3306 - options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + tests: + runs-on: ubuntu-latest + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + redis-socketio: + image: redis:alpine + ports: + - 12000:6379 + mariadb: + image: mariadb:10.6 + ports: + - 3306:3306 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_CHARACTER_SET: utf8mb4 + MARIADB_COLLATION_SERVER: utf8mb4_unicode_ci + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + steps: + - name: Checkout code + uses: actions/checkout@v2 - steps: - - name: Clone - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.11' - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" + - name: Set up Node + uses: actions/setup-node@v2 + with: + node-version: 18 + check-latest: true - - name: Check for valid Python & Merge Conflicts - run: | - python -m compileall -q -f "${GITHUB_WORKSPACE}" - if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" - then echo "Found merge conflicts" - exit 1 - fi + - name: Cache Bench + uses: actions/cache@v2 + with: + path: ~/bench-cache + key: ${{ runner.os }} - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 18 - check-latest: true + - name: Install Bench + run: | + pip3 install frappe-bench + which bench - - name: Add to Hosts - run: | - echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts + - name: Initialize Bench + run: | + if [ -d ~/bench-cache/bench.tgz ] + then + (cd && tar xzf ~/bench-cache/bench.tgz) + else + bench init ~/frappe-bench --skip-redis-config-generation --skip-assets --python "$(which python)" + mkdir -p ~/bench-cache + (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) + fi - - name: Cache pip - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- + - name: Add LMS App + working-directory: /home/runner/frappe-bench + run: bench get-app lms $GITHUB_WORKSPACE - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - name: Create Bench Site + working-directory: /home/runner/frappe-bench + run: bench new-site --mariadb-root-password root --admin-password admin frappe.local - - uses: actions/cache@v3 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- + - name: Install LMS App + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local install-app lms - - name: Install Dependencies - run: | - bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh - bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - env: - BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} - AFTER: ${{ env.GITHUB_EVENT_PATH.after }} - TYPE: server - DB: ${{ matrix.db }} + - name: Setup Requirements + working-directory: /home/runner/frappe-bench + run: bench setup requirements --dev - - name: Run Tests - run: cd ~/frappe-bench/ && bench --site lms.test run-parallel-tests --app lms --total-builds 4 --build-number ${{ matrix.container }} - env: - SITE: lms.test - CI_BUILD_ID: ${{ github.run_id }} - BUILD_NUMBER: ${{ matrix.container }} - TOTAL_BUILDS: 2 + - name: Allow Tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local set-config allow_tests true - - name: Show bench output - if: ${{ always() }} - run: cat ~/frappe-bench/bench_start.log || true + - name: Build + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local build + - name: Run Tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local run-tests --app lms From 8e12cae91f1ca1681a16d3c1e9f2c43e75d227da Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 15:58:07 +0530 Subject: [PATCH 060/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ae67c73..f4838e75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,10 @@ jobs: node-version: 18 check-latest: true + - name: Change MariaDB Collation + run: | + mysql -h 127.0.0.1 -P 3306 -uroot -proot -e "ALTER DATABASE dbname CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci" + - name: Cache Bench uses: actions/cache@v2 with: From eecc9b53df8284b848d456ddf905c666630f5494 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 16:07:01 +0530 Subject: [PATCH 061/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4838e75..550d823e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,8 +28,7 @@ jobs: - 3306:3306 env: MARIADB_ROOT_PASSWORD: root - MARIADB_CHARACTER_SET: utf8mb4 - MARIADB_COLLATION_SERVER: utf8mb4_unicode_ci + MARIADB_DATABASE: dbname options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Checkout code From caf967f2e2352f0bcf1b05eac9dfea027210b35d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 16:13:45 +0530 Subject: [PATCH 062/112] ci: added collation server for mariadb --- .github/workflows/ci.yml | 88 +++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550d823e..49978e98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,6 @@ jobs: - 3306:3306 env: MARIADB_ROOT_PASSWORD: root - MARIADB_DATABASE: dbname options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Checkout code @@ -45,56 +44,51 @@ jobs: node-version: 18 check-latest: true - - name: Change MariaDB Collation - run: | - mysql -h 127.0.0.1 -P 3306 -uroot -proot -e "ALTER DATABASE dbname CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci" + - name: Add to Hosts + run: echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts - - name: Cache Bench + - name: Cache pip uses: actions/cache@v2 with: - path: ~/bench-cache - key: ${{ runner.os }} + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- - - name: Install Bench + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install run: | - pip3 install frappe-bench - which bench - - - name: Initialize Bench - run: | - if [ -d ~/bench-cache/bench.tgz ] - then - (cd && tar xzf ~/bench-cache/bench.tgz) - else - bench init ~/frappe-bench --skip-redis-config-generation --skip-assets --python "$(which python)" - mkdir -p ~/bench-cache - (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) - fi - - - name: Add LMS App - working-directory: /home/runner/frappe-bench - run: bench get-app lms $GITHUB_WORKSPACE - - - name: Create Bench Site - working-directory: /home/runner/frappe-bench - run: bench new-site --mariadb-root-password root --admin-password admin frappe.local - - - name: Install LMS App - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local install-app lms - - - name: Setup Requirements - working-directory: /home/runner/frappe-bench - run: bench setup requirements --dev - - - name: Allow Tests - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local set-config allow_tests true - - - name: Build - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local build + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + BRANCH_TO_CLONE: ${{ env.HR_BRANCH }} - name: Run Tests - working-directory: /home/runner/frappe-bench - run: bench --site frappe.local run-tests --app lms + run: cd ~/frappe-bench/ && bench --site lms.test run-parallel-tests --app lms --total-builds 2 --build-number ${{ matrix.container }} + env: + TYPE: server + CI_BUILD_ID: ${{ github.run_id }} + ORCHESTRATOR_URL: http://test-orchestrator.frappe.io From f27eecce1fcf639570be8f87c6277154cda5fabf Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 16:17:58 +0530 Subject: [PATCH 063/112] ci: added collation server for mariadb --- .github/helper/install.sh | 4 ++++ .github/workflows/ci.yml | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index b5661726..198ead37 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -2,6 +2,10 @@ set -e cd ~ || exit +sudo apt update +sudo apt remove mysql-server mysql-client +sudo apt install libcups2-dev redis-server mariadb-client-10.6 + echo "Setting Up Bench..." pip install frappe-bench diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49978e98..6703541e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,8 +83,6 @@ jobs: - name: Install run: | bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - env: - BRANCH_TO_CLONE: ${{ env.HR_BRANCH }} - name: Run Tests run: cd ~/frappe-bench/ && bench --site lms.test run-parallel-tests --app lms --total-builds 2 --build-number ${{ matrix.container }} From f2432d78ee3cf98ff1ad0f0f1f22feb2aa1d5cf5 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 16:29:47 +0530 Subject: [PATCH 064/112] ci: added collation server for mariadb --- .github/helper/install.sh | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 198ead37..b6b9d354 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -23,14 +23,16 @@ mkdir ~/frappe-bench/sites/lms.test cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/lms.test/site_config.json -mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'"; -mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"; +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; -mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_lms"; -mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'"; -mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'"; +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_lms"; +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'"; +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'"; -mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES"; +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"; + +cd ~/frappe-bench || exit echo "Setting Up Procfile..." @@ -40,11 +42,10 @@ sed -i 's/^schedule:/# schedule:/g' Procfile echo "Starting Bench..." bench start &> bench_start.log & - CI=Yes bench build & -build_pid=$! - bench --site lms.test reinstall --yes -bench --site lms.test install-app lms -wait $build_pid +bench get-app hrms + +bench --site lms.test install-app lms +bench setup requirements --dev From bc2dc679a8b26454f646f4fd3ed2cbd9435329b1 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 13 Oct 2023 18:15:49 +0530 Subject: [PATCH 065/112] fix: revert ci changes --- .github/helper/install.sh | 27 +++++----- .github/workflows/ci.yml | 100 +++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 72 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index b6b9d354..21bb9d9a 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -2,10 +2,6 @@ set -e cd ~ || exit -sudo apt update -sudo apt remove mysql-server mysql-client -sudo apt install libcups2-dev redis-server mariadb-client-10.6 - echo "Setting Up Bench..." pip install frappe-bench @@ -23,16 +19,14 @@ mkdir ~/frappe-bench/sites/lms.test cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/lms.test/site_config.json -mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"; -mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; -mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_lms"; -mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'"; -mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_lms"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'"; +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'"; -mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"; - -cd ~/frappe-bench || exit +mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES"; echo "Setting Up Procfile..." @@ -42,10 +36,11 @@ sed -i 's/^schedule:/# schedule:/g' Procfile echo "Starting Bench..." bench start &> bench_start.log & + CI=Yes bench build & +build_pid=$! + bench --site lms.test reinstall --yes - -bench get-app hrms - bench --site lms.test install-app lms -bench setup requirements --dev + +wait $build_pid \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6703541e..01ee036a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,9 @@ name: Server Tests - on: push: branches: - main pull_request: {} - jobs: tests: runs-on: ubuntu-latest @@ -23,70 +21,60 @@ jobs: ports: - 12000:6379 mariadb: - image: mariadb:10.6 + image: anandology/mariadb-utf8mb4:10.3 ports: - 3306:3306 env: - MARIADB_ROOT_PASSWORD: root + MYSQL_ROOT_PASSWORD: root options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python + - uses: actions/checkout@v2 + - name: setup python uses: actions/setup-python@v2 with: - python-version: '3.11' - - - name: Set up Node + python-version: '3.10' + - name: setup node uses: actions/setup-node@v2 with: - node-version: 18 + node-version: '18' check-latest: true - - - name: Add to Hosts - run: echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts - - - name: Cache pip + - name: setup cache for bench uses: actions/cache@v2 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install + path: ~/bench-cache + key: ${{ runner.os }} + - name: install bench run: | - bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - - - name: Run Tests - run: cd ~/frappe-bench/ && bench --site lms.test run-parallel-tests --app lms --total-builds 2 --build-number ${{ matrix.container }} - env: - TYPE: server - CI_BUILD_ID: ${{ github.run_id }} - ORCHESTRATOR_URL: http://test-orchestrator.frappe.io + pip3 install frappe-bench + which bench + - name: bench init + run: | + if [ -d ~/bench-cache/bench.tgz ] + then + (cd && tar xzf ~/bench-cache/bench.tgz) + else + bench init ~/frappe-bench --skip-redis-config-generation --skip-assets --python "$(which python)" + mkdir -p ~/bench-cache + (cd && tar czf ~/bench-cache/bench.tgz frappe-bench) + fi + - name: add lms app to bench + working-directory: /home/runner/frappe-bench + run: bench get-app lms $GITHUB_WORKSPACE + - name: create bench site + working-directory: /home/runner/frappe-bench + run: bench new-site --mariadb-root-password root --admin-password admin frappe.local + - name: install lms app + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local install-app lms + - name: setup requirements + working-directory: /home/runner/frappe-bench + run: bench setup requirements --dev + - name: allow tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local set-config allow_tests true + - name: bench build + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local build + - name: run tests + working-directory: /home/runner/frappe-bench + run: bench --site frappe.local run-tests --app lms \ No newline at end of file From 8effd5614f9c2d777ca7be1480c4d24700bd1424 Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Fri, 13 Oct 2023 18:14:11 +0100 Subject: [PATCH 066/112] remove cast operation from str to float. It cause loss of the bookmark data --- lms/lms/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index f7dbe489..a0065dea 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -150,7 +150,7 @@ def get_lesson_details(chapter): ], as_dict=True, ) - lesson_details.number = flt(f"{chapter.idx}.{row.idx}") + lesson_details.number = f"{chapter.idx}.{row.idx}" lesson_details.icon = get_lesson_icon(lesson_details.body) lessons.append(lesson_details) return lessons From a70290921644ad4711cdec84feda10e09cc0e37f Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Fri, 13 Oct 2023 18:32:15 +0100 Subject: [PATCH 067/112] add `LessonBookmark` which is a data structure that represents a bookmark. While the underlying data structure is a tuple, it makes it easy to abstract most of the logic we need and therefore allow the code to be more readable. --- lms/www/batch/learn.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index c77233b7..72499273 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -105,6 +105,57 @@ def get_page_extensions(context): e.set_context(context) return extensions +class LessonBookmark: + """ + This represents a simple data structure to represent a lesson bookmark. + """ + def __init__(self, lesson_number: str) -> None: + self.__test_param_or_raise_exception(lesson_number) + _lesson_number = f"{lesson_number}." if not "." in lesson_number else lesson_number + first, second = _lesson_number.split(".") + + self.__value = (int(first), int(second) or 0) # second would be "" if `lesson_number` is something like "7" + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.__value == other.value + return NotImplemented + + def __gt__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.__value > other.value + return False + + def __repr__(self) -> str: + return f"{self.__value[0]}.{self.__value[1]}" + + def __test_param_or_raise_exception(self, string: str) -> None: + """ + Tests that a given string is in the format "n" or "n.n" where n is a numeric + character. If the string does not match the expected format, `TypeError` is + raised. + """ + import re + import sys + expected_format = r'^\d+\.?\d*$' + + try: + if not re.match(expected_format, string): + raise TypeError("""Expected a 'str' in the format 'n' or 'n.n' where n + is a numeric character. Example: '7' or '7.10""") + except TypeError as e: + tb = sys.exc_info()[2] + raise TypeError("""Expected a 'str' in the format 'n' or 'n.n' where n + is a numeric character. Example: '7' or '7.10""").with_traceback(tb) + + @property + def value(self): + return self.__value + + @property + def readable_value(self): + return self.__repr__() + def get_neighbours(current, lessons): current = flt(current) From 038a7463e1f870d0dddbb3ea4687e5963bf8e0f5 Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Fri, 13 Oct 2023 18:39:11 +0100 Subject: [PATCH 068/112] update `get_neighbours` to use `LessonBookmark` and return the correct bookmark string --- lms/www/batch/learn.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index 72499273..a1030a28 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -158,10 +158,10 @@ class LessonBookmark: def get_neighbours(current, lessons): - current = flt(current) - numbers = sorted(lesson.number for lesson in lessons) - index = numbers.index(current) + _current = LessonBookmark(current) + numbers = sorted([LessonBookmark(lesson.number) for lesson in lessons]) + index = numbers.index(_current) return { - "prev": numbers[index - 1] if index - 1 >= 0 else None, - "next": numbers[index + 1] if index + 1 < len(numbers) else None, + "prev": numbers[index - 1].readable_value if index - 1 >= 0 else None, + "next": numbers[index + 1].readable_value if index + 1 < len(numbers) else None, } From 12bec14c92fa8e943c74d9f2906b1612c18ac5de Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 16 Oct 2023 19:52:36 +0530 Subject: [PATCH 069/112] 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 070/112] 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 083/112] 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 084/112] 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 085/112] 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 ) From d413acaef3e03d29aeb288e2a0008d50a2c9b619 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 26 Oct 2023 15:14:35 +0530 Subject: [PATCH 086/112] fix: evaluation slots --- lms/www/batches/batch.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lms/www/batches/batch.js b/lms/www/batches/batch.js index 74395a2b..111c1307 100644 --- a/lms/www/batches/batch.js +++ b/lms/www/batches/batch.js @@ -517,6 +517,10 @@ const open_evaluation_form = (e) => { }, filter_description: " ", only_select: 1, + change: () => { + this.eval_form.set_value("date", ""); + $("[data-fieldname='slots']").html(""); + }, }, { fieldtype: "Date", @@ -527,7 +531,7 @@ const open_evaluation_form = (e) => { frappe.datetime.add_days(frappe.datetime.get_today(), 1) ), change: () => { - get_slots(); + if (this.eval_form.get_value("date")) get_slots(); }, }, { @@ -552,7 +556,7 @@ const get_slots = () => { args: { course: this.eval_form.get_value("course"), date: this.eval_form.get_value("date"), - batch_name: $(".class-details").data("batch"), + batch: $(".class-details").data("batch"), }, callback: (r) => { if (r.message) { From 243277012faa390ee7ec01a5ea6f7db4ccce6bcc Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 26 Oct 2023 17:51:43 +0530 Subject: [PATCH 087/112] feat: batch source --- lms/install.py | 23 +++++-- .../doctype/batch_student/batch_student.json | 17 +++-- lms/lms/doctype/lms_payment/lms_payment.json | 9 ++- lms/lms/doctype/lms_source/__init__.py | 0 lms/lms/doctype/lms_source/lms_source.js | 8 +++ lms/lms/doctype/lms_source/lms_source.json | 69 +++++++++++++++++++ lms/lms/doctype/lms_source/lms_source.py | 9 +++ lms/lms/doctype/lms_source/test_lms_source.py | 9 +++ lms/lms/utils.py | 10 +-- lms/patches.txt | 3 +- lms/patches/v1_0/create_batch_source.py | 5 ++ lms/www/billing/billing.js | 13 +++- 12 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 lms/lms/doctype/lms_source/__init__.py create mode 100644 lms/lms/doctype/lms_source/lms_source.js create mode 100644 lms/lms/doctype/lms_source/lms_source.json create mode 100644 lms/lms/doctype/lms_source/lms_source.py create mode 100644 lms/lms/doctype/lms_source/test_lms_source.py create mode 100644 lms/patches/v1_0/create_batch_source.py diff --git a/lms/install.py b/lms/install.py index 459d2c38..e77cad23 100644 --- a/lms/install.py +++ b/lms/install.py @@ -4,11 +4,11 @@ from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to def after_install(): add_pages_to_nav() + create_batch_source() def after_sync(): create_lms_roles() - set_default_home() set_default_certificate_print_format() add_all_roles_to("Administrator") @@ -64,10 +64,6 @@ def delete_lms_roles(): frappe.db.delete("Role", role) -def set_default_home(): - frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses") - - def create_course_creator_role(): if not frappe.db.exists("Role", "Course Creator"): role = frappe.get_doc( @@ -182,3 +178,20 @@ def delete_custom_fields(): for field in fields: frappe.db.delete("Custom Field", {"fieldname": field}) + + +def create_batch_source(): + sources = [ + "Newsletter", + "LinkedIn", + "Twitter", + "Website", + "Friend/Colleague/Connection", + "Google Search", + ] + + for source in sources: + if not frappe.db.exists("LMS Batch Source", source): + doc = frappe.new_doc("LMS Batch Source") + doc.source = source + doc.save() diff --git a/lms/lms/doctype/batch_student/batch_student.json b/lms/lms/doctype/batch_student/batch_student.json index 313c79c4..2fa00921 100644 --- a/lms/lms/doctype/batch_student/batch_student.json +++ b/lms/lms/doctype/batch_student/batch_student.json @@ -9,11 +9,12 @@ "field_order": [ "student_details_section", "student", - "payment", - "confirmation_email_sent", - "column_break_oduu", "student_name", - "username" + "username", + "column_break_oduu", + "payment", + "source", + "confirmation_email_sent" ], "fields": [ { @@ -59,12 +60,18 @@ "fieldname": "confirmation_email_sent", "fieldtype": "Check", "label": "Confirmation Email Sent" + }, + { + "fieldname": "source", + "fieldtype": "Link", + "label": "Source", + "options": "LMS Source" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-09 17:09:50.481794", + "modified": "2023-10-26 16:52:04.266693", "modified_by": "Administrator", "module": "LMS", "name": "Batch Student", diff --git a/lms/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json index c0019aef..82fb9388 100644 --- a/lms/lms/doctype/lms_payment/lms_payment.json +++ b/lms/lms/doctype/lms_payment/lms_payment.json @@ -10,6 +10,7 @@ "field_order": [ "payment_for_document_type", "member", + "source", "column_break_rqkd", "payment_for_document", "billing_name", @@ -129,11 +130,17 @@ "fieldtype": "Dynamic Link", "label": "Payment for Document", "options": "payment_for_document_type" + }, + { + "fieldname": "source", + "fieldtype": "Link", + "label": "Source", + "options": "LMS Source" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-17 23:17:50.334975", + "modified": "2023-10-26 16:54:12.408274", "modified_by": "Administrator", "module": "LMS", "name": "LMS Payment", diff --git a/lms/lms/doctype/lms_source/__init__.py b/lms/lms/doctype/lms_source/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_source/lms_source.js b/lms/lms/doctype/lms_source/lms_source.js new file mode 100644 index 00000000..e3c82001 --- /dev/null +++ b/lms/lms/doctype/lms_source/lms_source.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("LMS Source", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_source/lms_source.json b/lms/lms/doctype/lms_source/lms_source.json new file mode 100644 index 00000000..a1361696 --- /dev/null +++ b/lms/lms/doctype/lms_source/lms_source.json @@ -0,0 +1,69 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:source", + "creation": "2023-10-26 16:28:53.932278", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "source" + ], + "fields": [ + { + "fieldname": "source", + "fieldtype": "Data", + "label": "Source", + "unique": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-10-26 17:25:09.144367", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Source", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Moderator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "LMS Student", + "select": 1, + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "source" +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_source/lms_source.py b/lms/lms/doctype/lms_source/lms_source.py new file mode 100644 index 00000000..cf881d0d --- /dev/null +++ b/lms/lms/doctype/lms_source/lms_source.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LMSSource(Document): + pass diff --git a/lms/lms/doctype/lms_source/test_lms_source.py b/lms/lms/doctype/lms_source/test_lms_source.py new file mode 100644 index 00000000..99b7e97f --- /dev/null +++ b/lms/lms/doctype/lms_source/test_lms_source.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSSource(FrappeTestCase): + pass diff --git a/lms/lms/utils.py b/lms/lms/utils.py index b41af0dd..38fc5933 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -898,7 +898,7 @@ def check_multicurrency(amount, currency, country=None): currency = "USD" if apply_rounding and amount % 100 != 0: - amount = ceil(amount + 100 - amount % 100) + amount = amount + 100 - amount % 100 return amount, currency @@ -1028,12 +1028,13 @@ def record_payment(address, response, client, doctype, docname): "amount_with_gst": payment_details["amount_with_gst"], "gstin": address.gstin, "pan": address.pan, + "source": address.source, "payment_for_document_type": doctype, "payment_for_document": docname, } ) payment_doc.save(ignore_permissions=True) - return payment_doc.name + return payment_doc def get_payment_details(doctype, docname, address): @@ -1056,7 +1057,7 @@ def get_payment_details(doctype, docname, address): def create_membership(course, payment): membership = frappe.new_doc("LMS Enrollment") membership.update( - {"member": frappe.session.user, "course": course, "payment": payment} + {"member": frappe.session.user, "course": course, "payment": payment.name} ) membership.save(ignore_permissions=True) return f"/courses/{course}/learn/1.1" @@ -1067,7 +1068,8 @@ def add_student_to_batch(batchname, payment): student.update( { "student": frappe.session.user, - "payment": payment, + "payment": payment.name, + "source": payment.source, "parent": batchname, "parenttype": "LMS Batch", "parentfield": "students", diff --git a/lms/patches.txt b/lms/patches.txt index 3b2e34a9..1eaa79d8 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -75,4 +75,5 @@ 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 #26-10-2023 \ No newline at end of file +lms.patches.v1_0.add_certificate_template #26-10-2023 +lms.patches.v1_0.create_batch_source \ No newline at end of file diff --git a/lms/patches/v1_0/create_batch_source.py b/lms/patches/v1_0/create_batch_source.py new file mode 100644 index 00000000..5c70da3d --- /dev/null +++ b/lms/patches/v1_0/create_batch_source.py @@ -0,0 +1,5 @@ +from lms.install import create_batch_source + + +def execute(): + create_batch_source() diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index 3be9b310..ba387345 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -40,15 +40,15 @@ const setup_billing = () => { reqd: 1, default: address && address.city, }, - { - fieldtype: "Column Break", - }, { fieldtype: "Data", label: __("State/Province"), fieldname: "state", default: address && address.state, }, + { + fieldtype: "Column Break", + }, { fieldtype: "Link", label: __("Country"), @@ -75,6 +75,13 @@ const setup_billing = () => { reqd: 1, default: address && address.phone, }, + { + fieldtype: "Link", + label: __("Where did you hear about this?"), + fieldname: "source", + options: "LMS Source", + only_select: 1, + }, { fieldtype: "Section Break", label: __("GST Details"), From bb23b78a4f75f981728661433277000bf0076b84 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 26 Oct 2023 18:00:11 +0530 Subject: [PATCH 088/112] fix: made source mandatory in billing form --- lms/www/billing/billing.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index ba387345..484f9bb4 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -81,6 +81,7 @@ const setup_billing = () => { fieldname: "source", options: "LMS Source", only_select: 1, + reqd: 1, }, { fieldtype: "Section Break", From cb6013a7a6cadbe31bfb1b7a6a420f325bb91fdf Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 26 Oct 2023 18:09:05 +0530 Subject: [PATCH 089/112] fix: source doctype name during install --- lms/install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/install.py b/lms/install.py index e77cad23..cd74c28d 100644 --- a/lms/install.py +++ b/lms/install.py @@ -191,7 +191,7 @@ def create_batch_source(): ] for source in sources: - if not frappe.db.exists("LMS Batch Source", source): - doc = frappe.new_doc("LMS Batch Source") + if not frappe.db.exists("LMS Source", source): + doc = frappe.new_doc("LMS Source") doc.source = source doc.save() From 8c0c09a21b2b6032e1ac38259dc029ea3e1a86b1 Mon Sep 17 00:00:00 2001 From: saadindictrans Date: Fri, 27 Oct 2023 11:33:18 +0530 Subject: [PATCH 090/112] Fix:timetable validation --- lms/lms/doctype/lms_batch/lms_batch.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index ea368a7e..cf371f55 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -12,6 +12,7 @@ from frappe.utils import ( cint, format_date, format_datetime, + get_time, ) from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url from lms.www.utils import get_quiz_details, get_assignment_details @@ -116,23 +117,27 @@ class LMSBatch(Document): def validate_timetable(self): for schedule in self.timetable: if schedule.start_time and schedule.end_time: - if ( - schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time - ): + if get_time(schedule.start_time) > get_time(schedule.end_time) or get_time( + schedule.start_time + ) == get_time(schedule.end_time): frappe.throw( _("Row #{0} Start time cannot be greater than or equal to end time.").format( schedule.idx ) ) - if schedule.start_time < self.start_time or schedule.start_time > self.end_time: + if get_time(schedule.start_time) < get_time(self.start_time) or get_time( + schedule.start_time + ) > get_time(self.end_time): frappe.throw( _("Row #{0} Start time cannot be outside the batch duration.").format( schedule.idx ) ) - if schedule.end_time < self.start_time or schedule.end_time > self.end_time: + if get_time(schedule.end_time) < get_time(self.start_time) or get_time( + schedule.end_time + ) > get_time(self.end_time): frappe.throw( _("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx) ) From a49871c5b19d28b2cd3d2cacd53ec5aae8b98d7d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 27 Oct 2023 16:04:03 +0530 Subject: [PATCH 091/112] fix: misc batch issues --- lms/www/batches/index.py | 3 ++- lms/www/billing/billing.js | 52 ++++++++++++++++++++++++++++++++++++-- lms/www/billing/billing.py | 10 +++++--- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/lms/www/batches/index.py b/lms/www/batches/index.py index e16105bf..6112fdc2 100644 --- a/lms/www/batches/index.py +++ b/lms/www/batches/index.py @@ -48,7 +48,7 @@ def get_context(context): else: upcoming_batches.append(batch) - context.past_batches = sorted(past_batches, key=lambda d: d.start_date) + context.past_batches = sorted(past_batches, key=lambda d: d.start_date, reverse=True) context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date) context.private_batches = sorted(private_batches, key=lambda d: d.start_date) @@ -83,5 +83,6 @@ def get_context(context): batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count my_batches_info.append(batchinfo) + my_batches_info = sorted(my_batches_info, key=lambda d: d.start_date, reverse=True) context.my_batches = my_batches_info diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index 3be9b310..1c891b0e 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -106,6 +106,7 @@ const setup_billing = () => { const generate_payment_link = (e) => { let new_address = this.billing.get_values(); + validate_address(new_address); let doctype = $(e.currentTarget).attr("data-doctype"); let docname = decodeURIComponent($(e.currentTarget).attr("data-name")); @@ -174,8 +175,10 @@ const change_currency = () => { if (current_price != data.message) { update_price(data.message); } - if (!data.message.includes("INR")) { - $("#gst-message").addClass("hide"); + if (data.message.includes("INR")) { + $("#gst-message").removeClass("hide").addClass("show"); + } else { + $("#gst-message").removeClass("show").addClass("hide"); } }, }); @@ -188,3 +191,48 @@ const update_price = (price) => { indicator: "yellow", }); }; + +const validate_address = (billing_address) => { + if (billing_address.country == "India" && !billing_address.state) + frappe.throw(__("State is mandatory.")); + + const states = [ + "Andhra Pradesh", + "Arunachal Pradesh", + "Assam", + "Bihar", + "Chhattisgarh", + "Goa", + "Gujarat", + "Haryana", + "Himachal Pradesh", + "Jharkhand", + "Karnataka", + "Kerala", + "Madhya Pradesh", + "Maharashtra", + "Manipur", + "Meghalaya", + "Mizoram", + "Nagaland", + "Odisha", + "Punjab", + "Rajasthan", + "Sikkim", + "Tamil Nadu", + "Telangana", + "Tripura", + "Uttar Pradesh", + "Uttarakhand", + "West Bengal", + ]; + if ( + billing_address.country == "India" && + !states.includes(billing_address.state) + ) + frappe.throw( + __( + "Please enter a valid state with correct spelling and the first letter capitalized." + ) + ); +}; diff --git a/lms/www/billing/billing.py b/lms/www/billing/billing.py index ad1d45d3..d91013de 100644 --- a/lms/www/billing/billing.py +++ b/lms/www/billing/billing.py @@ -15,6 +15,13 @@ def get_context(context): validate_access(doctype, docname, module) get_billing_details(context) + context.original_currency = context.currency + context.original_amount = ( + apply_gst(context.amount, None)[0] + if context.original_currency == "INR" + else context.amount + ) + context.exception_country = frappe.get_all( "Payment Country", filters={"parent": "LMS Settings"}, pluck="country" ) @@ -27,9 +34,6 @@ def get_context(context): if context.currency == "INR": context.amount, context.gst_applied = apply_gst(context.amount, None) - context.original_amount = context.amount - context.original_currency = context.currency - def validate_access(doctype, docname, module): if frappe.session.user == "Guest": From 7b3f4c29d8e55c682dc9c1a07089decc5b5b1862 Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Fri, 27 Oct 2023 12:00:44 +0100 Subject: [PATCH 092/112] remove LessonBookmark abstraction. --- lms/www/batch/learn.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index a1030a28..ef1668ef 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -158,10 +158,13 @@ class LessonBookmark: def get_neighbours(current, lessons): - _current = LessonBookmark(current) - numbers = sorted([LessonBookmark(lesson.number) for lesson in lessons]) - index = numbers.index(_current) + numbers = [lesson.number for lesson in lessons] + tuples_list = [tuple(map(int, s.split('.'))) for s in numbers] + sorted_tuples = sorted(tuples_list) + sorted_numbers = ['.'.join(map(str, t)) for t in sorted_tuples] + index = sorted_numbers.index(current) + return { - "prev": numbers[index - 1].readable_value if index - 1 >= 0 else None, - "next": numbers[index + 1].readable_value if index + 1 < len(numbers) else None, + "prev": sorted_numbers[index - 1] if index - 1 >= 0 else None, + "next": sorted_numbers[index + 1] if index + 1 < len(sorted_numbers) else None, } From d67faa161036b836bbb5cbb9eb4b65ddba83e2f7 Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Fri, 27 Oct 2023 12:05:37 +0100 Subject: [PATCH 093/112] forgot to remove the `LessonBookmark` class --- lms/www/batch/learn.py | 51 ------------------------------------------ 1 file changed, 51 deletions(-) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index ef1668ef..cafeb5be 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -105,57 +105,6 @@ def get_page_extensions(context): e.set_context(context) return extensions -class LessonBookmark: - """ - This represents a simple data structure to represent a lesson bookmark. - """ - def __init__(self, lesson_number: str) -> None: - self.__test_param_or_raise_exception(lesson_number) - _lesson_number = f"{lesson_number}." if not "." in lesson_number else lesson_number - first, second = _lesson_number.split(".") - - self.__value = (int(first), int(second) or 0) # second would be "" if `lesson_number` is something like "7" - - def __eq__(self, other: object) -> bool: - if isinstance(other, self.__class__): - return self.__value == other.value - return NotImplemented - - def __gt__(self, other: object) -> bool: - if isinstance(other, self.__class__): - return self.__value > other.value - return False - - def __repr__(self) -> str: - return f"{self.__value[0]}.{self.__value[1]}" - - def __test_param_or_raise_exception(self, string: str) -> None: - """ - Tests that a given string is in the format "n" or "n.n" where n is a numeric - character. If the string does not match the expected format, `TypeError` is - raised. - """ - import re - import sys - expected_format = r'^\d+\.?\d*$' - - try: - if not re.match(expected_format, string): - raise TypeError("""Expected a 'str' in the format 'n' or 'n.n' where n - is a numeric character. Example: '7' or '7.10""") - except TypeError as e: - tb = sys.exc_info()[2] - raise TypeError("""Expected a 'str' in the format 'n' or 'n.n' where n - is a numeric character. Example: '7' or '7.10""").with_traceback(tb) - - @property - def value(self): - return self.__value - - @property - def readable_value(self): - return self.__repr__() - def get_neighbours(current, lessons): numbers = [lesson.number for lesson in lessons] From b44428677eef5a0962c2419da13d53a4599634a1 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 27 Oct 2023 17:17:10 +0530 Subject: [PATCH 094/112] chore: created security policy --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..6784170b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security). + +We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process. From b4af82acbc68df5bce4501c21ef22d9f0ad8c6fd Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 27 Oct 2023 17:21:28 +0530 Subject: [PATCH 095/112] chore: fix linters --- lms/www/batch/learn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index cafeb5be..0e233aac 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -108,9 +108,9 @@ def get_page_extensions(context): def get_neighbours(current, lessons): numbers = [lesson.number for lesson in lessons] - tuples_list = [tuple(map(int, s.split('.'))) for s in numbers] + tuples_list = [tuple(map(int, s.split("."))) for s in numbers] sorted_tuples = sorted(tuples_list) - sorted_numbers = ['.'.join(map(str, t)) for t in sorted_tuples] + sorted_numbers = [".".join(map(str, t)) for t in sorted_tuples] index = sorted_numbers.index(current) return { From 1af547288cc79d5acdaca9e1ab41546d6cf4f73f Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 27 Oct 2023 17:53:35 +0530 Subject: [PATCH 096/112] chore: fix linters --- lms/www/batch/learn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index 0e233aac..a3b9566c 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -108,9 +108,9 @@ def get_page_extensions(context): def get_neighbours(current, lessons): numbers = [lesson.number for lesson in lessons] - tuples_list = [tuple(map(int, s.split("."))) for s in numbers] + tuples_list = [tuple(map(int, s.split("."))) for s in numbers] # noqa sorted_tuples = sorted(tuples_list) - sorted_numbers = [".".join(map(str, t)) for t in sorted_tuples] + sorted_numbers = [".".join(map(str, t)) for t in sorted_tuples] # noqa index = sorted_numbers.index(current) return { From 05282178dd639ae20eb0320a462b64a71d1ffc00 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Fri, 27 Oct 2023 18:30:45 +0530 Subject: [PATCH 097/112] fix: removed functional programing code --- lms/www/batch/learn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index a3b9566c..d51751ac 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -108,9 +108,9 @@ def get_page_extensions(context): def get_neighbours(current, lessons): numbers = [lesson.number for lesson in lessons] - tuples_list = [tuple(map(int, s.split("."))) for s in numbers] # noqa + tuples_list = [tuple(int(x) for x in s.split(".")) for s in numbers] sorted_tuples = sorted(tuples_list) - sorted_numbers = [".".join(map(str, t)) for t in sorted_tuples] # noqa + sorted_numbers = [".".join(str(num) for num in t) for t in sorted_tuples] index = sorted_numbers.index(current) return { From 69591577bfaf3050e7813ca6ed38f7f83a028a03 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 30 Oct 2023 18:30:58 +0530 Subject: [PATCH 098/112] feat: batch tabs settings --- .../doctype/lms_settings/lms_settings.json | 87 +++++++++--- lms/patches.txt | 6 +- lms/patches/v1_0/batch_tabs_settings.py | 16 +++ lms/patches/v1_0/create_batch_source.py | 2 + lms/www/batches/batch.html | 133 ++++++++++-------- lms/www/batches/batch.py | 2 +- 6 files changed, 173 insertions(+), 73 deletions(-) create mode 100644 lms/patches/v1_0/batch_tabs_settings.py diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index 13b98a9f..abfbb361 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -17,17 +17,15 @@ "section_break_szgq", "send_calendar_invite_for_evaluations", "batch_confirmation_template", - "column_break_2", "allow_student_progress", - "payment_section", - "razorpay_key", - "razorpay_secret", - "apply_gst", - "column_break_cfcv", - "default_currency", - "show_usd_equivalent", - "apply_rounding", - "exception_country", + "column_break_2", + "show_dashboard", + "show_courses", + "show_students", + "show_assessments", + "show_live_class", + "show_discussions", + "show_emails", "signup_settings_tab", "signup_settings_section", "terms_of_use", @@ -42,7 +40,17 @@ "mentor_request_tab", "mentor_request_section", "mentor_request_creation", - "mentor_request_status_update" + "mentor_request_status_update", + "payment_settings_tab", + "payment_section", + "razorpay_key", + "razorpay_secret", + "apply_gst", + "column_break_cfcv", + "default_currency", + "show_usd_equivalent", + "apply_rounding", + "exception_country" ], "fields": [ { @@ -71,7 +79,8 @@ }, { "fieldname": "column_break_2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "label": "Show Tab in Batch" }, { "fieldname": "search_placeholder", @@ -177,7 +186,7 @@ { "fieldname": "section_break_szgq", "fieldtype": "Section Break", - "label": "Batch Settings" + "label": "Class Settings" }, { "fieldname": "signup_settings_tab", @@ -199,8 +208,7 @@ }, { "fieldname": "payment_section", - "fieldtype": "Section Break", - "label": "Payment" + "fieldtype": "Section Break" }, { "fieldname": "default_currency", @@ -261,12 +269,59 @@ "fieldtype": "Link", "label": "Batch Confirmation Template", "options": "Email Template" + }, + { + "default": "1", + "fieldname": "show_courses", + "fieldtype": "Check", + "label": "Courses" + }, + { + "default": "1", + "fieldname": "show_students", + "fieldtype": "Check", + "label": "Students" + }, + { + "default": "1", + "fieldname": "show_assessments", + "fieldtype": "Check", + "label": "Assessments" + }, + { + "default": "1", + "fieldname": "show_live_class", + "fieldtype": "Check", + "label": "Live Class" + }, + { + "default": "1", + "fieldname": "show_discussions", + "fieldtype": "Check", + "label": "Discussions" + }, + { + "default": "1", + "fieldname": "show_emails", + "fieldtype": "Check", + "label": "Emails" + }, + { + "fieldname": "payment_settings_tab", + "fieldtype": "Tab Break", + "label": "Payment Settings" + }, + { + "default": "1", + "fieldname": "show_dashboard", + "fieldtype": "Check", + "label": "Dashboard" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-09 17:27:28.615355", + "modified": "2023-10-30 16:42:58.994359", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", diff --git a/lms/patches.txt b/lms/patches.txt index 1eaa79d8..7a6fba08 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -1,3 +1,4 @@ +[pre_model_sync] community.patches.set_email_preferences community.patches.change_name_for_community_members community.patches.save_abbr_for_community_members @@ -76,4 +77,7 @@ 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 #26-10-2023 -lms.patches.v1_0.create_batch_source \ No newline at end of file +lms.patches.v1_0.create_batch_source + +[post_model_sync] +lms.patches.v1_0.batch_tabs_settings \ No newline at end of file diff --git a/lms/patches/v1_0/batch_tabs_settings.py b/lms/patches/v1_0/batch_tabs_settings.py new file mode 100644 index 00000000..3fec8329 --- /dev/null +++ b/lms/patches/v1_0/batch_tabs_settings.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + fields = [ + "show_dashboard", + "show_courses", + "show_students", + "show_emails", + "show_assessments", + "show_discussions", + "show_live_class", + ] + + for field in fields: + frappe.db.set_single_value("LMS Settings", field, 1) diff --git a/lms/patches/v1_0/create_batch_source.py b/lms/patches/v1_0/create_batch_source.py index 5c70da3d..c6c8f54b 100644 --- a/lms/patches/v1_0/create_batch_source.py +++ b/lms/patches/v1_0/create_batch_source.py @@ -1,5 +1,7 @@ +import frappe from lms.install import create_batch_source def execute(): + frappe.reload_doc("lms", "doctype", "lms_source") create_batch_source() diff --git a/lms/www/batches/batch.html b/lms/www/batches/batch.html index cda89302..0183f5f5 100644 --- a/lms/www/batches/batch.html +++ b/lms/www/batches/batch.html @@ -88,8 +88,7 @@