-
+
diff --git a/.github/helper/install.sh b/.github/helper/install.sh index b5661726..21bb9d9a 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -43,4 +43,4 @@ build_pid=$! bench --site lms.test reinstall --yes bench --site lms.test install-app lms -wait $build_pid +wait $build_pid \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aafbfa01..01ee036a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,5 +77,4 @@ jobs: run: bench --site frappe.local build - name: run tests working-directory: /home/runner/frappe-bench - run: bench --site frappe.local run-tests --app lms - + run: bench --site frappe.local run-tests --app lms \ No newline at end of file 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. diff --git a/cypress.config.js b/cypress.config.js index ffbf5130..04ebc494 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -13,6 +13,6 @@ module.exports = defineConfig({ openMode: 0, }, e2e: { - baseUrl: "http://dd1:8000", + baseUrl: "http://pyp:8000", }, }); diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 48f3c5dd..2be9a71c 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -33,19 +33,17 @@ 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( + "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(".ce-block:last").click().type("{enter}"); - cy.get(".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/fixtures/custom_field.json b/lms/fixtures/custom_field.json index cdb50dba..1d8f17da 100644 --- a/lms/fixtures/custom_field.json +++ b/lms/fixtures/custom_field.json @@ -138,12 +138,12 @@ "label": "User Category", "length": 0, "mandatory_depends_on": null, - "modified": "2022-04-19 13:02:18.219508", + "modified": "2022-04-19 13:02:18.219510", "module": "LMS", "name": "User-user_category", "no_copy": 0, "non_negative": 0, - "options": "Business Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers", + "options": "\nBusiness Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers", "permlevel": 0, "precision": "", "print_hide": 0, diff --git a/lms/hooks.py b/lms/hooks.py index e0b1461c..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 @@ -304,6 +303,9 @@ 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", + "Audio": "lms.plugins.audio_renderer", + "PDF": "lms.plugins.pdf_renderer", } # page_renderer to manage profile pages diff --git a/lms/install.py b/lms/install.py index 5856b839..cd74c28d 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") @@ -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(): @@ -63,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( @@ -106,6 +103,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", @@ -168,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 Source", source): + doc = frappe.new_doc("LMS Source") + doc.source = source + doc.save() 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/batch_student/batch_student.json b/lms/lms/doctype/batch_student/batch_student.json index b2a8816a..2fa00921 100644 --- a/lms/lms/doctype/batch_student/batch_student.json +++ b/lms/lms/doctype/batch_student/batch_student.json @@ -9,10 +9,12 @@ "field_order": [ "student_details_section", "student", - "payment", - "column_break_oduu", "student_name", - "username" + "username", + "column_break_oduu", + "payment", + "source", + "confirmation_email_sent" ], "fields": [ { @@ -52,12 +54,24 @@ "fieldtype": "Link", "label": "Payment", "options": "LMS Payment" + }, + { + "default": "0", + "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-08-24 17:48:53.045539", + "modified": "2023-10-26 16:52:04.266693", "modified_by": "Administrator", "module": "LMS", "name": "Batch Student", 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 eaee75f2..d5879b00 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -24,6 +24,7 @@ "file_type", "section_break_11", "body", + "instructor_notes", "help_section", "help" ], @@ -131,11 +132,16 @@ { "fieldname": "column_break_15", "fieldtype": "Column Break" + }, + { + "fieldname": "instructor_notes", + "fieldtype": "Markdown Editor", + "label": "Instructor Notes" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-02 12:42:16.926753", + "modified": "2023-09-29 17:04:19.252897", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", @@ -163,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/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/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/invite_request/invite_request.py b/lms/lms/doctype/invite_request/invite_request.py index 7848529c..6a1d335b 100644 --- a/lms/lms/doctype/invite_request/invite_request.py +++ b/lms/lms/doctype/invite_request/invite_request.py @@ -11,7 +11,11 @@ from frappe.utils.password import get_decrypted_password class InviteRequest(Document): def on_update(self): - if self.has_value_changed("status") and self.status == "Approved": + if ( + self.has_value_changed("status") + and self.status == "Approved" + and not frappe.flags.in_test + ): self.send_email() def create_user(self, password): diff --git a/lms/lms/doctype/lms_assignment/lms_assignment.json b/lms/lms/doctype/lms_assignment/lms_assignment.json index 6cb778a1..654989d5 100644 --- a/lms/lms/doctype/lms_assignment/lms_assignment.json +++ b/lms/lms/doctype/lms_assignment/lms_assignment.json @@ -9,10 +9,12 @@ "engine": "InnoDB", "field_order": [ "title", + "grade_assignment", + "question", "column_break_hmwv", "type", - "section_break_lwvt", - "question" + "show_answer", + "answer" ], "fields": [ { @@ -26,7 +28,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "Document\nPDF\nURL\nImage" + "options": "Document\nPDF\nURL\nImage\nText" }, { "fieldname": "title", @@ -40,13 +42,29 @@ "fieldtype": "Column Break" }, { - "fieldname": "section_break_lwvt", - "fieldtype": "Section Break" + "default": "0", + "depends_on": "eval:doc.type == \"Text\"", + "fieldname": "show_answer", + "fieldtype": "Check", + "label": "Show Answer" + }, + { + "depends_on": "show_answer", + "fieldname": "answer", + "fieldtype": "Text Editor", + "label": "Answer" + }, + { + "default": "1", + "depends_on": "eval:doc.type == \"Text\"", + "fieldname": "grade_assignment", + "fieldtype": "Check", + "label": "Grade Assignment" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-26 18:09:29.809564", + "modified": "2023-10-06 12:08:46.898950", "modified_by": "Administrator", "module": "LMS", "name": "LMS Assignment", 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_assignment_submission/lms_assignment_submission.js b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.js index d6aec59c..8a999c77 100644 --- a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.js +++ b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.js @@ -2,6 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on("LMS Assignment Submission", { - // refresh: function(frm) { - // } + onload: function (frm) { + frm.set_query("member", function (doc) { + return { + filters: { + ignore_user_type: 1, + }, + }; + }); + }, }); diff --git a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json index 1d68e33b..86c71c4d 100644 --- a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json +++ b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json @@ -9,29 +9,29 @@ "field_order": [ "assignment", "assignment_title", - "question", + "type", "column_break_3", "member", "member_name", - "type", + "section_break_dlzh", + "question", + "column_break_zvis", "assignment_attachment", "answer", "section_break_rqal", "status", + "evaluator", "column_break_esgd", "comments", "section_break_cwaw", "lesson", "course", - "column_break_ygdu", - "evaluator" + "column_break_ygdu" ], "fields": [ { "fieldname": "lesson", "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, "label": "Lesson", "options": "Course Lesson" }, @@ -78,7 +78,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Pass\nFail\nNot Graded" + "options": "Pass\nFail\nNot Graded\nNot Applicable" }, { "fieldname": "comments", @@ -94,7 +94,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.type != \"URL\";", + "depends_on": "eval:!([\"URL\", \"Text\"]).includes(doc.type);", "fieldname": "assignment_attachment", "fieldtype": "Attach", "label": "Assignment Attachment", @@ -104,8 +104,9 @@ "fetch_from": "assignment.type", "fieldname": "type", "fieldtype": "Select", + "in_list_view": 1, "label": "Type", - "options": "Document\nPDF\nURL\nImage" + "options": "Document\nPDF\nURL\nImage\nText" }, { "fetch_from": "assignment.question", @@ -137,17 +138,25 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.type == \"URL\";", + "depends_on": "eval:([\"URL\", \"Text\"]).includes(doc.type);", "fieldname": "answer", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Answer", "mandatory_depends_on": "eval:doc.type == \"URL\";" + }, + { + "fieldname": "section_break_dlzh", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_zvis", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-08-30 12:09:03.332820", + "modified": "2023-10-06 15:14:55.984714", "modified_by": "Administrator", "module": "LMS", "name": "LMS Assignment Submission", @@ -181,6 +190,10 @@ { "color": "Red", "title": "Fail" + }, + { + "color": "Blue", + "title": "Not Applicable" } ], "title_field": "assignment_title" diff --git a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.py b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.py index 1989f87c..a58f2207 100644 --- a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.py +++ b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.py @@ -5,12 +5,17 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import validate_url +from frappe.email.doctype.email_template.email_template import get_email_template class LMSAssignmentSubmission(Document): def validate(self): self.validate_duplicates() + def after_insert(self): + if not frappe.flags.in_test: + self.send_mail() + def validate_duplicates(self): if frappe.db.exists( "LMS Assignment Submission", @@ -23,6 +28,35 @@ class LMSAssignmentSubmission(Document): ) ) + def send_mail(self): + subject = _("New Assignment Submission") + template = "assignment_submission" + custom_template = frappe.db.get_single_value( + "LMS Settings", "assignment_submission_template" + ) + + args = { + "member_name": self.member_name, + "assignment_name": self.assignment, + "assignment_title": self.assignment_title, + "submission_name": self.name, + } + + moderators = frappe.get_all("Has Role", {"role": "Moderator"}, pluck="parent") + + if custom_template: + email_template = get_email_template(custom_template, args) + subject = email_template.get("subject") + content = email_template.get("message") + frappe.sendmail( + recipients=moderators, + subject=subject, + template=template if not custom_template else None, + content=content if custom_template else None, + args=args, + header=[subject, "green"], + ) + @frappe.whitelist() def upload_assignment( @@ -37,9 +71,12 @@ def upload_assignment( if frappe.session.user == "Guest": return - assignment_type = frappe.db.get_value("LMS Assignment", assignment, "type") + assignment_details = frappe.db.get_value( + "LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1 + ) + assignment_type = assignment_details.type - if assignment_type == "URL" and not answer: + if assignment_type in ["URL", "Text"] and not answer: frappe.throw(_("Please enter the URL for assignment submission.")) if assignment_type == "File" and not assignment_attachment: @@ -64,7 +101,9 @@ def upload_assignment( doc.update( { "assignment_attachment": assignment_attachment, - "status": status, + "status": "Not Applicable" + if assignment_type == "Text" and not assignment_details.grade_assignment + else status, "comments": comments, "answer": answer, } diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js index 45f9d0b5..34f9d503 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -10,25 +10,116 @@ frappe.ui.form.on("LMS Batch", { }, }; }); - }, - fetch_lessons: (frm) => { - frm.clear_table("scheduled_flow"); - 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("scheduled_flow"); - row.lesson = lesson.name; - row.lesson_title = lesson.title; - }); - frm.refresh_field("scheduled_flow"); - } - }, + frm.set_query("reference_doctype", "timetable", function () { + 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], + }, + }; }); }, + + timetable_template: function (frm) { + 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", + "milestone", + ], + 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"); + child.reference_doctype = row.reference_doctype; + child.reference_docname = row.reference_docname; + child.date = frappe.datetime.add_days(frm.doc.start_date, row.day - 1); + child.start_time = row.start_time; + child.end_time = row.end_time + ? row.end_time + : row.duration + ? moment + .utc(row.start_time, "HH:mm") + .add(row.duration, "hour") + .format("HH:mm") + : null; + child.duration = row.duration; + child.milestone = row.milestone; + }); + 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 26965356..f0838a4b 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -14,6 +14,7 @@ "column_break_4", "start_time", "end_time", + "published", "section_break_rgfj", "medium", "category", @@ -21,21 +22,34 @@ "seat_count", "section_break_6", "description", + "batch_details_raw", + "column_break_hlqw", "batch_details", + "meta_image", + "section_break_jgji", "students", "courses", + "assessment_tab", + "assessment", + "schedule_tab", + "timetable_template", + "column_break_anya", + "show_live_class", + "allow_future", + "section_break_ontp", + "timetable", + "timetable_legends", + "pricing_tab", "section_break_gsac", "paid_batch", "column_break_iens", "amount", "currency", + "customisations_tab", "section_break_ubxi", "custom_component", - "assessment_tab", - "assessment", - "schedule_tab", - "fetch_lessons", - "scheduled_flow" + "column_break_pxgb", + "custom_script" ], "fields": [ { @@ -86,10 +100,9 @@ "reqd": 1 }, { - "description": "The HTML code entered here will be displayed on the batch details page.", "fieldname": "custom_component", "fieldtype": "Code", - "label": "Custom Component", + "label": "Custom HTML", "options": "HTML" }, { @@ -142,33 +155,23 @@ }, { "fieldname": "category", - "fieldtype": "Autocomplete", - "label": "Category" - }, - { - "fieldname": "scheduled_flow", - "fieldtype": "Table", - "label": "Scheduled Flow", - "options": "Scheduled Flow" + "fieldtype": "Link", + "label": "Category", + "options": "LMS Category" }, { + "description": "These customisations will work on the main batch page.", "fieldname": "section_break_ubxi", "fieldtype": "Section Break" }, - { - "fieldname": "fetch_lessons", - "fieldtype": "Button", - "label": "Fetch Lessons" - }, { "fieldname": "schedule_tab", "fieldtype": "Tab Break", - "label": "Schedule" + "label": "Timetable" }, { "fieldname": "section_break_gsac", - "fieldtype": "Section Break", - "label": "Pricing" + "fieldtype": "Section Break" }, { "fieldname": "column_break_iens", @@ -192,11 +195,93 @@ "fieldtype": "Text Editor", "label": "Batch Details", "reqd": 1 + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" + }, + { + "fieldname": "timetable", + "fieldtype": "Table", + "label": "Timetable", + "options": "LMS Batch Timetable" + }, + { + "fieldname": "timetable_template", + "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" + }, + { + "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" + }, + { + "fieldname": "column_break_pxgb", + "fieldtype": "Column Break" + }, + { + "fieldname": "customisations_tab", + "fieldtype": "Tab Break", + "label": "Customisations" + }, + { + "fieldname": "pricing_tab", + "fieldtype": "Tab Break", + "label": "Pricing" + }, + { + "fieldname": "custom_script", + "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-08-23 17:35:42.750754", + "modified": "2023-10-12 12:53:37.351989", "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 5a0b0467..cf371f55 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -6,9 +6,17 @@ 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 +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 +from frappe.email.doctype.email_template.email_template import get_email_template class LMSBatch(Document): @@ -19,7 +27,8 @@ class LMSBatch(Document): self.validate_duplicate_students() self.validate_duplicate_assessments() self.validate_membership() - self.validate_schedule() + self.validate_timetable() + self.send_confirmation_mail() def validate_duplicate_students(self): students = [row.student for row in self.students] @@ -53,6 +62,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: @@ -68,26 +114,30 @@ 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 - ): + 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) ) @@ -192,6 +242,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, @@ -201,6 +253,7 @@ def create_batch( amount=0, currency=None, name=None, + published=0, ): frappe.only_for("Moderator") if name: @@ -215,6 +268,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, @@ -223,6 +278,7 @@ def create_batch( "paid_batch": paid_batch, "amount": amount, "currency": currency, + "published": published, } ) doc.save() @@ -243,6 +299,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: @@ -260,3 +320,119 @@ 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", + "milestone", + "name", + "idx", + "parent", + ], + 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" + ) + assessment = frappe._dict({"assessment_name": entry.reference_docname}) + + if entry.reference_doctype == "Course Lesson": + course = frappe.db.get_value( + entry.reference_doctype, entry.reference_docname, "course" + ) + entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname)) + + entry.completed = ( + True + if frappe.db.exists( + "LMS Course Progress", + {"lesson": entry.reference_docname, "member": frappe.session.user}, + ) + else False + ) + + elif entry.reference_doctype == "LMS Quiz": + entry.url = "/quizzes" + details = get_quiz_details(assessment, frappe.session.user) + entry.update(details) + + elif entry.reference_doctype == "LMS Assignment": + details = get_assignment_details(assessment, frappe.session.user) + entry.update(details) + + timetable = sorted(timetable, key=lambda k: k["date"]) + return timetable + + +@frappe.whitelist() +def is_milestone_complete(idx, batch): + previous_rows = frappe.get_all( + "LMS Batch Timetable", + filters={"parent": batch, "idx": ["<", cint(idx)]}, + fields=["reference_doctype", "reference_docname", "idx"], + order_by="idx", + ) + + for row in previous_rows: + if row.reference_doctype == "Course Lesson": + if not frappe.db.exists( + "LMS Course Progress", + {"member": frappe.session.user, "lesson": row.reference_docname}, + ): + return False + + if row.reference_doctype == "LMS Quiz": + passing_percentage = frappe.db.get_value( + row.reference_doctype, row.reference_docname, "passing_percentage" + ) + if not frappe.db.exists( + "LMS Quiz Submission", + {"quiz": row.reference_docname, "member": frappe.session.user}, + ): + return False + + if row.reference_doctype == "LMS Assignment": + if not frappe.db.exists( + "LMS Assignment Submission", + {"assignment": row.reference_docname, "member": frappe.session.user}, + ): + return False + + return True diff --git a/lms/lms/notification/assignment_submission_notification/__init__.py b/lms/lms/doctype/lms_batch_timetable/__init__.py similarity index 100% rename from lms/lms/notification/assignment_submission_notification/__init__.py rename to lms/lms/doctype/lms_batch_timetable/__init__.py 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..cbf51bd8 --- /dev/null +++ b/lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json @@ -0,0 +1,93 @@ +{ + "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", + "day", + "column_break_merq", + "start_time", + "end_time", + "duration", + "milestone" + ], + "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" + }, + { + "depends_on": "eval:doc.parenttype == \"LMS Batch\";", + "fieldname": "date", + "fieldtype": "Date", + "label": "Date" + }, + { + "fieldname": "start_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "Start Time" + }, + { + "fieldname": "duration", + "fieldtype": "Data", + "label": "Duration" + }, + { + "fieldname": "column_break_htdc", + "fieldtype": "Column Break" + }, + { + "fieldname": "end_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "End Time" + }, + { + "depends_on": "eval: doc.parenttype == \"LMS Timetable Template\";", + "fieldname": "day", + "fieldtype": "Int", + "label": "Day" + }, + { + "fieldname": "milestone", + "fieldtype": "Check", + "label": "Milestone" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-10-20 11:58:01.782921", + "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_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 39206779..83f4ba52 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.json +++ b/lms/lms/doctype/lms_certificate/lms_certificate.json @@ -8,10 +8,12 @@ "course", "member", "member_name", + "template", "column_break_3", "issue_date", "expiry_date", - "batch_name" + "batch_name", + "published" ], "fields": [ { @@ -60,11 +62,24 @@ "in_standard_filter": 1, "label": "Batch", "options": "LMS Batch" + }, + { + "default": "0", + "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-08-29 15:23:08.637215", + "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..f7e1193e 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.py +++ b/lms/lms/doctype/lms_certificate/lms_certificate.py @@ -6,12 +6,42 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import add_years, nowdate from lms.lms.utils import is_certified +from frappe.email.doctype.email_template.email_template import get_email_template class LMSCertificate(Document): def validate(self): self.validate_duplicate_certificate() + def after_insert(self): + if not frappe.flags.in_test: + self.send_mail() + + def send_mail(self): + subject = _("Congratulations on getting certified!") + template = "certification" + custom_template = frappe.db.get_single_value("LMS Settings", "certification_template") + + args = { + "student_name": self.member_name, + "course_name": self.course, + "course_title": frappe.db.get_value("LMS Course", self.course, "title"), + "certificate_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=self.member, + subject=subject, + template=template if not custom_template else None, + content=content if custom_template else None, + args=args, + header=[subject, "green"], + ) + def validate_duplicate_certificate(self): certificates = frappe.get_all( "LMS Certificate", @@ -48,6 +78,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 +94,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/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json b/lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json index eb66ee71..998ce8bc 100644 --- a/lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json +++ b/lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json @@ -84,7 +84,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Pass\nFail", + "options": "Pending\nIn Progress\nPass\nFail", "reqd": 1 }, { @@ -106,7 +106,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-23 14:51:21.947169", + "modified": "2023-09-26 19:44:43.594892", "modified_by": "Administrator", "module": "LMS", "name": "LMS Certificate Evaluation", @@ -139,6 +139,23 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [], + "states": [ + { + "color": "Green", + "title": "Pass" + }, + { + "color": "Red", + "title": "Fail" + }, + { + "color": "Blue", + "title": "Pending" + }, + { + "color": "Orange", + "title": "In Progress" + } + ], "title_field": "member_name" } \ No newline at end of file diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index cd6f184f..9065b639 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( { @@ -217,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: @@ -281,6 +280,7 @@ def save_lesson( preview, idx, lesson, + instructor_notes=None, youtube=None, quiz_id=None, question=None, @@ -296,6 +296,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/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/lms/doctype/lms_live_class/lms_live_class.json b/lms/lms/doctype/lms_live_class/lms_live_class.json index 1e68dbfc..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-03-14 18:44:48.813103", + "modified": "2023-09-20 11:29:20.899897", "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/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json index f89bc595..82fb9388 100644 --- a/lms/lms/doctype/lms_payment/lms_payment.json +++ b/lms/lms/doctype/lms_payment/lms_payment.json @@ -8,13 +8,17 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "payment_for_document_type", "member", + "source", "column_break_rqkd", + "payment_for_document", "billing_name", "payment_received", "payment_details_section", - "amount", "currency", + "amount", + "amount_with_gst", "column_break_yxpl", "order_id", "payment_id", @@ -39,7 +43,8 @@ "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, - "label": "Amount" + "label": "Amount", + "options": "currency" }, { "fieldname": "currency", @@ -107,11 +112,35 @@ "label": "Member", "options": "User", "reqd": 1 + }, + { + "depends_on": "eval:doc.currency == \"INR\";", + "fieldname": "amount_with_gst", + "fieldtype": "Currency", + "label": "Amount with GST" + }, + { + "fieldname": "payment_for_document_type", + "fieldtype": "Select", + "label": "Payment for Document Type", + "options": "\nLMS Course\nLMS Batch" + }, + { + "fieldname": "payment_for_document", + "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-08-24 22:08:12.294960", + "modified": "2023-10-26 16:54:12.408274", "modified_by": "Administrator", "module": "LMS", "name": "LMS Payment", 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..90f820c7 --- /dev/null +++ b/lms/lms/doctype/lms_question/lms_question.json @@ -0,0 +1,245 @@ +{ + "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", + "possibility_1", + "possibility_3", + "column_break_wpjr", + "possibility_2", + "possibility_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": "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": "possibility_3", + "fieldtype": "Small Text", + "label": "Possible Answer 3" + }, + { + "fieldname": "possibility_2", + "fieldtype": "Small Text", + "label": "Possible Answer 2" + }, + { + "fieldname": "possibility_4", + "fieldtype": "Small Text", + "label": "Possible Answer 4" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-10-18 21:58:42.653317", + "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 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Moderator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Course Creator", + "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..0e6f87bd --- /dev/null +++ b/lms/lms/doctype/lms_question/lms_question.py @@ -0,0 +1,92 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from lms.lms.utils import has_course_instructor_role, has_course_moderator_role + + +class LMSQuestion(Document): + def validate(self): + validate_correct_answers(self) + + +def validate_correct_answers(question): + 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.")) + + +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 validate_possible_answer(question): + possible_answers = [] + possible_answers_fields = [ + "possibility_1", + "possibility_2", + "possibility_3", + "possibility_4", + ] + + for field in possible_answers_fields: + if question.get(field): + possible_answers.append(field) + + if not len(possible_answers): + frappe.throw( + _("Add at least one possible answer for this question: {0}").format( + frappe.bold(question.question) + ) + ) + + +def get_correct_options(question): + correct_options = [] + correct_option_fields = [ + "is_correct_1", + "is_correct_2", + "is_correct_3", + "is_correct_4", + ] + for field in correct_option_fields: + if question.get(field) == 1: + correct_options.append(field) + + return correct_options + + +@frappe.whitelist() +def get_question_details(question): + if not has_course_instructor_role() or not has_course_moderator_role(): + return + + fields = ["question", "type", "name"] + for i in range(1, 5): + fields.append(f"option_{i}") + fields.append(f"is_correct_{i}") + fields.append(f"explanation_{i}") + fields.append(f"possibility_{i}") + + return frappe.db.get_value("LMS Question", question, fields, as_dict=1) 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.js b/lms/lms/doctype/lms_quiz/lms_quiz.js index 095938be..0f6f4729 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.js +++ b/lms/lms/doctype/lms_quiz/lms_quiz.js @@ -5,3 +5,13 @@ frappe.ui.form.on("LMS Quiz", { // refresh: function(frm) { // } }); + +frappe.ui.form.on("LMS Quiz Question", { + marks: function (frm) { + total_marks = 0; + frm.doc.questions.forEach((question) => { + total_marks += question.marks; + }); + frm.doc.total_marks = total_marks; + }, +}); diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json index dbbea28b..a00e1617 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", @@ -43,7 +47,7 @@ "read_only": 1 }, { - "default": "1", + "default": "0", "fieldname": "max_attempts", "fieldtype": "Int", "label": "Max Attempts" @@ -90,11 +94,35 @@ "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", + "non_negative": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_rocd", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "total_marks", + "fieldtype": "Int", + "label": "Total Marks", + "non_negative": 1, + "read_only": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-04 15:26:24.457745", + "modified": "2023-11-07 10:11:49.126789", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz", @@ -123,6 +151,18 @@ "role": "Moderator", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Course Creator", + "share": 1, + "write": 1 } ], "show_title_field_in_link": 1, diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 9cb5789c..709cfe34 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -5,18 +5,32 @@ import json 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 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, + has_course_moderator_role, + has_course_instructor_role, +) class LMSQuiz(Document): + def validate(self): + self.validate_duplicate_questions() + self.total_marks = set_total_marks(self.name, self.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") - 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,76 +49,11 @@ 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): +def set_total_marks(quiz, questions): + marks = 0 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} - ) + marks += question.get("marks") + return marks @frappe.whitelist() @@ -114,45 +63,72 @@ def quiz_summary(quiz, results): for result in results: correct = result["is_correct"][0] - result["question"] = frappe.db.get_value( - "LMS Quiz Question", - {"parent": quiz, "idx": result["question_index"] + 1}, - ["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, + "percentage": percentage, + "passing_percentage": quiz_details.passing_percentage, } ) submission.save(ignore_permissions=True) return { "score": score, + "score_out_of": score_out_of, "submission": submission.name, + "pass": percentage == quiz_details.passing_percentage, } @frappe.whitelist() def save_quiz( - quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0 + quiz_title, + passing_percentage, + questions, + max_attempts=0, + 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 = { "title": quiz_title, + "passing_percentage": passing_percentage, "max_attempts": max_attempts, "show_answers": show_answers, "show_submission_history": show_submission_history, @@ -160,41 +136,77 @@ def save_quiz( if quiz: frappe.db.set_value("LMS Quiz", quiz, values) + update_questions(quiz, questions) return quiz else: doc = frappe.new_doc("LMS Quiz") doc.update(values) - doc.save(ignore_permissions=True) + doc.save() + update_questions(doc.name, questions) return doc.name +def update_questions(quiz, questions): + questions = json.loads(questions) + + delete_questions(quiz, questions) + add_questions(quiz, questions) + frappe.db.set_value("LMS Quiz", quiz, "total_marks", set_total_marks(quiz, questions)) + + +def delete_questions(quiz, questions): + existing_questions = frappe.get_all( + "LMS Quiz Question", + { + "parent": quiz, + }, + pluck="name", + ) + + current_questions = [question.get("question_name") for question in questions] + + for question in existing_questions: + if question not in current_questions: + frappe.db.delete("LMS Quiz Question", question) + + +def add_questions(quiz, questions): + for index, question in enumerate(questions): + question = frappe._dict(question) + if question.question_name: + doc = frappe.get_doc("LMS Quiz Question", question.question_name) + else: + doc = frappe.new_doc("LMS Quiz Question") + doc.update( + { + "parent": quiz, + "parenttype": "LMS Quiz", + "parentfield": "questions", + "idx": index + 1, + } + ) + + doc.update({"question": question.question, "marks": question.marks}) + + doc.save() + + @frappe.whitelist() def save_question(quiz, values, index): values = frappe._dict(json.loads(values)) - validate_correct_answers([values]) if values.get("name"): - doc = frappe.get_doc("LMS Quiz Question", values.get("name")) + doc = frappe.get_doc("LMS Question", values.get("name")) else: - doc = frappe.new_doc("LMS Quiz Question") + doc = frappe.new_doc("LMS Question") doc.update( { - "question": values["question"], + "question": values.question, "type": values["type"], } ) - if not values.get("name"): - doc.update( - { - "parent": quiz, - "parenttype": "LMS Quiz", - "parentfield": "questions", - "idx": index, - } - ) - for num in range(1, 5): if values.get(f"option_{num}"): doc.update( @@ -218,9 +230,8 @@ def save_question(quiz, values, index): } ) - doc.save(ignore_permissions=True) - - return quiz + doc.save() + return doc.name @frappe.whitelist() @@ -253,9 +264,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: @@ -271,9 +280,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/test_lms_quiz.py b/lms/lms/doctype/lms_quiz/test_lms_quiz.py index ee79b89a..ca6a998c 100644 --- a/lms/lms/doctype/lms_quiz/test_lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/test_lms_quiz.py @@ -10,51 +10,36 @@ import frappe class TestLMSQuiz(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save( - ignore_permissions=True - ) + frappe.get_doc( + {"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90} + ).save(ignore_permissions=True) def test_with_multiple_options(self): - quiz = frappe.get_doc("LMS Quiz", "test-quiz") - quiz.append( - "questions", - { - "question": "Question Multiple", - "type": "Choices", - "option_1": "Option 1", - "is_correct_1": 1, - "option_2": "Option 2", - "is_correct_2": 1, - }, - ) - quiz.save() - self.assertTrue(quiz.questions[0].multiple) + question = frappe.new_doc("LMS Question") + question.question = "Question Multiple" + question.type = "Choices" + question.option_1 = "Option 1" + question.is_correct_1 = 1 + question.option_2 = "Option 2" + question.is_correct_2 = 1 + question.save() + self.assertTrue(question.multiple) def test_with_no_correct_option(self): - quiz = frappe.get_doc("LMS Quiz", "test-quiz") - quiz.append( - "questions", - { - "question": "Question no correct option", - "type": "Choices", - "option_1": "Option 1", - "option_2": "Option 2", - }, - ) - self.assertRaises(frappe.ValidationError, quiz.save) + question = frappe.new_doc("LMS Question") + question.question = "Question Multiple" + question.type = "Choices" + question.option_1 = "Option 1" + question.option_2 = "Option 2" + self.assertRaises(frappe.ValidationError, question.save) def test_with_no_possible_answers(self): - quiz = frappe.get_doc("LMS Quiz", "test-quiz") - quiz.append( - "questions", - { - "question": "Question Possible Answers", - "type": "User Input", - }, - ) - self.assertRaises(frappe.ValidationError, quiz.save) + question = frappe.new_doc("LMS Question") + question.question = "Question Multiple" + question.type = "User Input" + self.assertRaises(frappe.ValidationError, question.save) @classmethod def tearDownClass(cls) -> None: frappe.db.delete("LMS Quiz", "test-quiz") - frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"}) + frappe.db.delete("LMS Question") 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..e14a8d48 100644 --- a/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json +++ b/lms/lms/doctype/lms_quiz_question/lms_quiz_question.json @@ -6,208 +6,31 @@ "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" + "marks" ], "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", + "default": "1", + "fieldname": "marks", + "fieldtype": "Int", "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" + "label": "Marks", + "non_negative": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-04 16:43:49.837134", + "modified": "2023-10-16 19:51:03.893144", "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..7c8fcfac 100644 --- a/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json +++ b/lms/lms/doctype/lms_quiz_result/lms_quiz_result.json @@ -6,7 +6,11 @@ "engine": "InnoDB", "field_order": [ "question", + "section_break_fztv", + "question_name", "answer", + "column_break_flus", + "marks", "is_correct" ], "fields": [ @@ -31,12 +35,33 @@ "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" + }, + { + "fieldname": "marks", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Marks", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-24 11:15:45.931119", + "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..84d63e77 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,16 @@ "engine": "InnoDB", "field_order": [ "quiz", - "score", "course", "column_break_3", "member", "member_name", + "section_break_dkpn", + "score", + "score_out_of", + "column_break_gkip", + "percentage", + "passing_percentage", "section_break_6", "result" ], @@ -31,9 +36,11 @@ }, { "fieldname": "score", - "fieldtype": "Data", + "fieldtype": "Int", "in_list_view": 1, - "label": "Score" + "label": "Score", + "read_only": 1, + "reqd": 1 }, { "fieldname": "member", @@ -65,12 +72,45 @@ "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 + }, + { + "fieldname": "passing_percentage", + "fieldtype": "Int", + "label": "Passing 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.979975", "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..9d7050d5 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,10 @@ from frappe.model.document import Document class LMSQuizSubmission(Document): - pass + def before_insert(self): + if not self.percentage: + 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/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index 4e1442c5..c69e4d8f 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -16,14 +16,15 @@ "portal_course_creation", "section_break_szgq", "send_calendar_invite_for_evaluations", - "column_break_2", "allow_student_progress", - "payment_section", - "razorpay_key", - "default_currency", - "column_break_cfcv", - "razorpay_secret", - "apply_gst", + "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", @@ -38,7 +39,22 @@ "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", + "email_templates_tab", + "certification_template", + "batch_confirmation_template", + "column_break_uwsp", + "assignment_submission_template" ], "fields": [ { @@ -67,7 +83,8 @@ }, { "fieldname": "column_break_2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "label": "Show Tab in Batch" }, { "fieldname": "search_placeholder", @@ -173,7 +190,7 @@ { "fieldname": "section_break_szgq", "fieldtype": "Section Break", - "label": "Class Settings" + "label": "Batch Settings" }, { "fieldname": "signup_settings_tab", @@ -183,6 +200,7 @@ { "fieldname": "mentor_request_tab", "fieldtype": "Tab Break", + "hidden": 1, "label": "Mentor Request" }, { @@ -194,8 +212,7 @@ }, { "fieldname": "payment_section", - "fieldtype": "Section Break", - "label": "Payment" + "fieldtype": "Section Break" }, { "fieldname": "default_currency", @@ -231,12 +248,105 @@ "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" + }, + { + "fieldname": "batch_confirmation_template", + "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" + }, + { + "fieldname": "certification_template", + "fieldtype": "Link", + "label": "Certificate Email Template", + "options": "Email Template" + }, + { + "fieldname": "email_templates_tab", + "fieldtype": "Tab Break", + "label": "Email Templates" + }, + { + "fieldname": "assignment_submission_template", + "fieldtype": "Link", + "label": "Assignment Submission Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_uwsp", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-29 09:54:48.030823", + "modified": "2023-11-07 11:23:14.257687", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", 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/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/__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..b5ec2d9b --- /dev/null +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.js @@ -0,0 +1,29 @@ +// 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"]; + return { + filters: { + name: ["in", doctypes], + }, + }; + }); + + 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 new file mode 100644 index 00000000..3b016b7a --- /dev/null +++ b/lms/lms/doctype/lms_timetable_template/lms_timetable_template.json @@ -0,0 +1,72 @@ +{ + "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", + "timetable_legends" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "timetable", + "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-10-11 17:09:05.096243", + "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/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/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/notification/assignment_submission_notification/assignment_submission_notification.json b/lms/lms/notification/assignment_submission_notification/assignment_submission_notification.json deleted file mode 100644 index 03a91be8..00000000 --- a/lms/lms/notification/assignment_submission_notification/assignment_submission_notification.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "attach_print": 0, - "channel": "Email", - "creation": "2023-03-27 16:34:03.505647", - "days_in_advance": 0, - "docstatus": 0, - "doctype": "Notification", - "document_type": "LMS Assignment Submission", - "enabled": 1, - "event": "New", - "idx": 0, - "is_standard": 1, - "message": "
{{ _(\"{0} has submitted their assignment for the lesson {1}\").format(doc.member_name, title) }}
\n\n{{ _(\" Please evaluate and grade the assignment. \") }}
", - "modified": "2023-03-27 16:46:44.564007", - "modified_by": "Administrator", - "module": "LMS", - "name": "Assignment Submission Notification", - "owner": "Administrator", - "recipients": [ - { - "receiver_by_document_field": "evaluator" - } - ], - "send_system_notification": 0, - "send_to_all_assignees": 0, - "subject": "Assignment Submission" -} \ No newline at end of file diff --git a/lms/lms/notification/assignment_submission_notification/assignment_submission_notification.md b/lms/lms/notification/assignment_submission_notification/assignment_submission_notification.md deleted file mode 100644 index 02b2f997..00000000 --- a/lms/lms/notification/assignment_submission_notification/assignment_submission_notification.md +++ /dev/null @@ -1,11 +0,0 @@ -{{ _("{0} has submitted their assignment for the lesson {1}").format(frappe.bold(doc.member_name), frappe.bold(title)) }} -
-{{ _(" Please evaluate and grade the assignment.") }}
-{{ doc.name }}
\\n| \n \n \n {{ _(\"Course Instructor\") }} \n | \n {% endif %}\n \n {% if certificate.expiry_date %}\n \n \n | \n \n \n {{ _(\"Expiry Date\") }} \n | \n {% endif %}\n
| \n \n \n {{ _(\"Course Instructor\") }} \n | \n {% endif %}\n \n {% if certificate.expiry_date %}\n \n \n | \n \n \n {{ _(\"Expiry Date\") }} \n | \n {% endif %}\n
+ {{ _("{0} has submitted the assignment {1}").format(frappe.bold(member_name), frappe.bold(assignment_title)) }} +
+{{ _(" Please evaluate and grade it.") }}
++ {{ _("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") }} +
+ diff --git a/lms/templates/emails/certification.html b/lms/templates/emails/certification.html new file mode 100644 index 00000000..3d47b1b9 --- /dev/null +++ b/lms/templates/emails/certification.html @@ -0,0 +1,21 @@ ++ {{ _("Dear ") }} {{ student_name }}, +
++ {{ _("I am delighted to inform you that you have successfully earned your certification for the {0} course. Congratulations!").format(frappe.bold(course_title)) }} +
++ {{ _("With this certification, you can now showcase your updated skills and share your achievement with your colleagues and on LinkedIn. To access your certificate, please click on the link provided below.") }} +
++ {{ _("Once again, congratulations on this significant accomplishment.")}} +
++ {{ _("Best Regards") }} +
\ No newline at end of file diff --git a/lms/templates/onboarding_header.html b/lms/templates/onboarding_header.html index 204e36ee..fdcd1cdf 100644 --- a/lms/templates/onboarding_header.html +++ b/lms/templates/onboarding_header.html @@ -4,7 +4,7 @@