diff --git a/README.md b/README.md index 371a642b..f77ddfe8 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ You can create courses and lessons through simple forms. Lessons can be in the f - Add detailed descriptions and preview videos to the course. ๐ŸŽฌ - Add videos, quizzes, and assignments to your lessons and make them interesting and interactive ๐Ÿ“ - Discussions section below each lesson where instructors and students can interact with each other. ๐Ÿ’ฌ -- Create classes to group your students based on courses and track their progress ๐Ÿ› +- Create batches to group your students based on courses and track their progress ๐Ÿ› - Statistics dashboard that provides all important numbers at a glimpse. ๐Ÿ“ˆ - Job Board where users can post and look for jobs. ๐Ÿ’ผ - People directory with each person's profile page ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ diff --git a/lms/hooks.py b/lms/hooks.py index e78659ab..ab6ac7b4 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -152,7 +152,7 @@ website_route_rules = [ }, {"from_route": "/quizzes", "to_route": "batch/quiz_list"}, {"from_route": "/quizzes/", "to_route": "batch/quiz"}, - {"from_route": "/classes/", "to_route": "classes/class"}, + {"from_route": "/batches/", "to_route": "batches/batch"}, {"from_route": "/courses//progress", "to_route": "batch/progress"}, {"from_route": "/courses//join", "to_route": "batch/join"}, {"from_route": "/courses//manage", "to_route": "cohorts"}, @@ -176,8 +176,8 @@ website_route_rules = [ {"from_route": "/users", "to_route": "profiles/profile"}, {"from_route": "/jobs/", "to_route": "jobs/job"}, { - "from_route": "/classes//students/", - "to_route": "/classes/progress", + "from_route": "/batches//students/", + "to_route": "/batches/progress", }, {"from_route": "/assignments/", "to_route": "assignments/assignment"}, { @@ -189,9 +189,17 @@ website_route_rules = [ "to_route": "quiz_submission/quiz_submission", }, { - "from_route": "/billing/", + "from_route": "/billing//", "to_route": "billing/billing", }, + { + "from_route": "/batches/details/", + "to_route": "batches/batch_details", + }, + { + "from_route": "/certified-participants", + "to_route": "certified_participants/certified_participants", + }, ] website_redirects = [ @@ -249,6 +257,7 @@ jinja = { "lms.lms.utils.can_create_courses", "lms.lms.utils.get_telemetry_boot_info", "lms.lms.utils.is_onboarding_complete", + "lms.www.utils.is_student", ], "filters": [], } diff --git a/lms/install.py b/lms/install.py index 3bd0539b..5856b839 100644 --- a/lms/install.py +++ b/lms/install.py @@ -17,7 +17,7 @@ def add_pages_to_nav(): pages = [ {"label": "Explore", "idx": 1}, {"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2}, - {"label": "Classes", "url": "/classes", "parent": "Explore", "idx": 3}, + {"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3}, {"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4}, {"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5}, {"label": "People", "url": "/community", "parent": "Explore", "idx": 6}, diff --git a/lms/lms/doctype/class_course/__init__.py b/lms/lms/doctype/batch_course/__init__.py similarity index 100% rename from lms/lms/doctype/class_course/__init__.py rename to lms/lms/doctype/batch_course/__init__.py diff --git a/lms/lms/doctype/class_course/class_course.json b/lms/lms/doctype/batch_course/batch_course.json similarity index 93% rename from lms/lms/doctype/class_course/class_course.json rename to lms/lms/doctype/batch_course/batch_course.json index 6412b74a..bf393be0 100644 --- a/lms/lms/doctype/class_course/class_course.json +++ b/lms/lms/doctype/batch_course/batch_course.json @@ -38,10 +38,10 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-13 17:51:36.278393", + "modified": "2023-08-28 10:03:02.960844", "modified_by": "Administrator", "module": "LMS", - "name": "Class Course", + "name": "Batch Course", "naming_rule": "Autoincrement", "owner": "Administrator", "permissions": [], diff --git a/lms/lms/doctype/class_course/class_course.py b/lms/lms/doctype/batch_course/batch_course.py similarity index 84% rename from lms/lms/doctype/class_course/class_course.py rename to lms/lms/doctype/batch_course/batch_course.py index 64b9f410..db2de9a2 100644 --- a/lms/lms/doctype/class_course/class_course.py +++ b/lms/lms/doctype/batch_course/batch_course.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class ClassCourse(Document): +class BatchCourse(Document): pass diff --git a/lms/lms/doctype/class_student/__init__.py b/lms/lms/doctype/batch_student/__init__.py similarity index 100% rename from lms/lms/doctype/class_student/__init__.py rename to lms/lms/doctype/batch_student/__init__.py diff --git a/lms/lms/doctype/class_student/class_student.js b/lms/lms/doctype/batch_student/batch_student.js similarity index 78% rename from lms/lms/doctype/class_student/class_student.js rename to lms/lms/doctype/batch_student/batch_student.js index 69567d23..680bc334 100644 --- a/lms/lms/doctype/class_student/class_student.js +++ b/lms/lms/doctype/batch_student/batch_student.js @@ -1,7 +1,7 @@ // Copyright (c) 2022, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on("Class Student", { +frappe.ui.form.on("Batch Student", { // refresh: function(frm) { // } }); diff --git a/lms/lms/doctype/class_student/class_student.json b/lms/lms/doctype/batch_student/batch_student.json similarity index 59% rename from lms/lms/doctype/class_student/class_student.json rename to lms/lms/doctype/batch_student/batch_student.json index 6c6857af..699050df 100644 --- a/lms/lms/doctype/class_student/class_student.json +++ b/lms/lms/doctype/batch_student/batch_student.json @@ -7,7 +7,10 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "student_details_section", "student", + "payment", + "column_break_oduu", "student_name", "username" ], @@ -24,22 +27,40 @@ "fetch_from": "student.full_name", "fieldname": "student_name", "fieldtype": "Data", - "label": "Student Name" + "in_list_view": 1, + "label": "Student Name", + "read_only": 1 }, { "fetch_from": "student.username", "fieldname": "username", "fieldtype": "Data", - "label": "Username" + "label": "Username", + "read_only": 1 + }, + { + "fieldname": "student_details_section", + "fieldtype": "Section Break", + "label": "Student Details" + }, + { + "fieldname": "column_break_oduu", + "fieldtype": "Column Break" + }, + { + "fieldname": "payment", + "fieldtype": "Link", + "label": "Payment", + "options": "LMS Payment" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-18 18:45:22.787839", + "modified": "2023-09-12 16:46:41.042810", "modified_by": "Administrator", "module": "LMS", - "name": "Class Student", + "name": "Batch Student", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/lms/lms/doctype/class_student/class_student.py b/lms/lms/doctype/batch_student/batch_student.py similarity index 84% rename from lms/lms/doctype/class_student/class_student.py rename to lms/lms/doctype/batch_student/batch_student.py index cfe37fa1..7cd9a61a 100644 --- a/lms/lms/doctype/class_student/class_student.py +++ b/lms/lms/doctype/batch_student/batch_student.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class ClassStudent(Document): +class BatchStudent(Document): pass diff --git a/lms/lms/doctype/class_student/test_class_student.py b/lms/lms/doctype/batch_student/test_batch_student.py similarity index 77% rename from lms/lms/doctype/class_student/test_class_student.py rename to lms/lms/doctype/batch_student/test_batch_student.py index 41f8ed09..88c576e4 100644 --- a/lms/lms/doctype/class_student/test_class_student.py +++ b/lms/lms/doctype/batch_student/test_batch_student.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestClassStudent(FrappeTestCase): +class TestBatchStudent(FrappeTestCase): pass diff --git a/lms/lms/doctype/course_evaluator/course_evaluator.py b/lms/lms/doctype/course_evaluator/course_evaluator.py index a7ed55a2..a3b03e10 100644 --- a/lms/lms/doctype/course_evaluator/course_evaluator.py +++ b/lms/lms/doctype/course_evaluator/course_evaluator.py @@ -37,8 +37,8 @@ class CourseEvaluator(Document): @frappe.whitelist() -def get_schedule(course, date, class_name=None): - evaluator = get_evaluator(course, class_name) +def get_schedule(course, date, batch=None): + evaluator = get_evaluator(course, batch) all_slots = frappe.get_all( "Evaluator Schedule", diff --git a/lms/lms/doctype/lms_class/__init__.py b/lms/lms/doctype/lms_batch/__init__.py similarity index 100% rename from lms/lms/doctype/lms_class/__init__.py rename to lms/lms/doctype/lms_batch/__init__.py diff --git a/lms/lms/doctype/lms_class/lms_class.js b/lms/lms/doctype/lms_batch/lms_batch.js similarity index 75% rename from lms/lms/doctype/lms_class/lms_class.js rename to lms/lms/doctype/lms_batch/lms_batch.js index 342b6ae2..45f9d0b5 100644 --- a/lms/lms/doctype/lms_class/lms_class.js +++ b/lms/lms/doctype/lms_batch/lms_batch.js @@ -1,12 +1,12 @@ // Copyright (c) 2022, Frappe and contributors // For license information, please see license.txt -frappe.ui.form.on("LMS Class", { +frappe.ui.form.on("LMS Batch", { onload: function (frm) { - frm.set_query("class_student", "students", function (doc) { + frm.set_query("student", "students", function (doc) { return { filters: { - class_name: doc.name, + ignore_user_type: 1, }, }; }); @@ -15,7 +15,7 @@ frappe.ui.form.on("LMS Class", { fetch_lessons: (frm) => { frm.clear_table("scheduled_flow"); frappe.call({ - method: "lms.lms.doctype.lms_class.lms_class.fetch_lessons", + method: "lms.lms.doctype.lms_batch.lms_batch.fetch_lessons", args: { courses: frm.doc.courses, }, diff --git a/lms/lms/doctype/lms_class/lms_class.json b/lms/lms/doctype/lms_batch/lms_batch.json similarity index 76% rename from lms/lms/doctype/lms_class/lms_class.json rename to lms/lms/doctype/lms_batch/lms_batch.json index d1dc5ff7..a7949ce3 100644 --- a/lms/lms/doctype/lms_class/lms_class.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -14,16 +14,22 @@ "column_break_4", "start_time", "end_time", + "published", "section_break_rgfj", "medium", "category", "column_break_flwy", "seat_count", - "paid_class", "section_break_6", "description", + "batch_details", "students", "courses", + "section_break_gsac", + "paid_batch", + "column_break_iens", + "amount", + "currency", "section_break_ubxi", "custom_component", "assessment_tab", @@ -65,13 +71,13 @@ "fieldname": "students", "fieldtype": "Table", "label": "Students", - "options": "Class Student" + "options": "Batch Student" }, { "fieldname": "courses", "fieldtype": "Table", "label": "Courses", - "options": "Class Course" + "options": "Batch Course" }, { "fieldname": "start_date", @@ -81,7 +87,7 @@ "reqd": 1 }, { - "description": "The HTML code entered here will be displayed on the class details page.", + "description": "The HTML code entered here will be displayed on the batch details page.", "fieldname": "custom_component", "fieldtype": "Code", "label": "Custom Component", @@ -89,9 +95,10 @@ }, { "default": "0", - "fieldname": "paid_class", + "description": "Students will be enrolled in a paid batch once they complete the payment", + "fieldname": "paid_batch", "fieldtype": "Check", - "label": "Paid Class" + "label": "Paid Batch" }, { "fieldname": "seat_count", @@ -158,14 +165,48 @@ "fieldname": "schedule_tab", "fieldtype": "Tab Break", "label": "Schedule" + }, + { + "fieldname": "section_break_gsac", + "fieldtype": "Section Break", + "label": "Pricing" + }, + { + "fieldname": "column_break_iens", + "fieldtype": "Column Break" + }, + { + "depends_on": "paid_batch", + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount" + }, + { + "depends_on": "paid_batch", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "batch_details", + "fieldtype": "Text Editor", + "label": "Batch Details", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-23 14:23:07.706539", + "modified": "2023-09-12 12:30:06.565104", "modified_by": "Administrator", "module": "LMS", - "name": "LMS Class", + "name": "LMS Batch", "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ @@ -192,18 +233,6 @@ "role": "Moderator", "share": 1, "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Class Evaluator", - "share": 1, - "write": 1 } ], "show_title_field_in_link": 1, diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_batch/lms_batch.py similarity index 78% rename from lms/lms/doctype/lms_class/lms_class.py rename to lms/lms/doctype/lms_batch/lms_batch.py index 29e0c414..3c6e034f 100644 --- a/lms/lms/doctype/lms_class/lms_class.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -11,7 +11,7 @@ from frappe.utils import cint, format_date, format_datetime from lms.lms.utils import get_lessons -class LMSClass(Document): +class LMSBatch(Document): def validate(self): if self.seat_count: self.validate_seats_left() @@ -26,7 +26,7 @@ class LMSClass(Document): duplicates = {student for student in students if students.count(student) > 1} if len(duplicates): frappe.throw( - _("Student {0} has already been added to this class.").format( + _("Student {0} has already been added to this batch.").format( frappe.bold(next(iter(duplicates))) ) ) @@ -37,7 +37,7 @@ class LMSClass(Document): if len(duplicates): title = frappe.db.get_value("LMS Course", next(iter(duplicates)), "title") frappe.throw( - _("Course {0} has already been added to this class.").format(frappe.bold(title)) + _("Course {0} has already been added to this batch.").format(frappe.bold(title)) ) def validate_duplicate_assessments(self): @@ -48,7 +48,7 @@ class LMSClass(Document): assessment.assessment_type, assessment.assessment_name, "title" ) frappe.throw( - _("Assessment {0} has already been added to this class.").format( + _("Assessment {0} has already been added to this batch.").format( frappe.bold(title) ) ) @@ -66,7 +66,7 @@ class LMSClass(Document): def validate_seats_left(self): if cint(self.seat_count) < len(self.students): - frappe.throw(_("There are no seats available in this class.")) + frappe.throw(_("There are no seats available in this batch.")) def validate_schedule(self): for schedule in self.scheduled_flow: @@ -82,32 +82,32 @@ class LMSClass(Document): if schedule.start_time < self.start_time or schedule.start_time > self.end_time: frappe.throw( - _("Row #{0} Start time cannot be outside the class duration.").format( + _("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: frappe.throw( - _("Row #{0} End time cannot be outside the class duration.").format(schedule.idx) + _("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx) ) if schedule.date < self.start_date or schedule.date > self.end_date: frappe.throw( - _("Row #{0} Date cannot be outside the class duration.").format(schedule.idx) + _("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx) ) @frappe.whitelist() -def remove_student(student, class_name): +def remove_student(student, batch_name): frappe.only_for("Moderator") - frappe.db.delete("Class Student", {"student": student, "parent": class_name}) + frappe.db.delete("Batch Student", {"student": student, "parent": batch_name}) @frappe.whitelist() def remove_course(course, parent): frappe.only_for("Moderator") - frappe.db.delete("Class Course", {"course": course, "parent": parent}) + frappe.db.delete("Batch Course", {"course": course, "parent": parent}) @frappe.whitelist() @@ -118,7 +118,7 @@ def remove_assessment(assessment, parent): @frappe.whitelist() def create_live_class( - class_name, title, duration, date, time, timezone, auto_recording, description=None + batch_name, title, duration, date, time, timezone, auto_recording, description=None ): date = format_date(date, "yyyy-mm-dd", True) frappe.only_for("Moderator") @@ -152,7 +152,7 @@ def create_live_class( "host": frappe.session.user, "date": date, "time": time, - "class_name": class_name, + "batch_name": batch_name, "password": data.get("password"), "description": description, "auto_recording": auto_recording, @@ -186,39 +186,49 @@ def authenticate(): @frappe.whitelist() -def create_class( +def create_batch( title, start_date, end_date, description=None, + batch_details=None, seat_count=0, start_time=None, end_time=None, medium="Online", category=None, + paid_batch=0, + amount=0, + currency=None, name=None, + published=0, ): frappe.only_for("Moderator") if name: - class_details = frappe.get_doc("LMS Class", name) + doc = frappe.get_doc("LMS Batch", name) else: - class_details = frappe.get_doc({"doctype": "LMS Class"}) + doc = frappe.get_doc({"doctype": "LMS Batch"}) - class_details.update( + doc.update( { "title": title, "start_date": start_date, "end_date": end_date, "description": description, + "batch_details": batch_details, "seat_count": seat_count, "start_time": start_time, "end_time": end_time, "medium": medium, "category": category, + "paid_batch": paid_batch, + "amount": amount, + "currency": currency, + "published": published, } ) - class_details.save() - return class_details + doc.save() + return doc @frappe.whitelist() @@ -230,3 +240,25 @@ def fetch_lessons(courses): lessons.extend(get_lessons(course.get("course"))) return lessons + + +@frappe.whitelist() +def add_course(course, parent, name=None, evaluator=None): + frappe.only_for("Moderator") + if name: + doc = frappe.get_doc("Batch Course", name) + else: + doc = frappe.new_doc("Batch Course") + + doc.update( + { + "course": course, + "evaluator": evaluator, + "parent": parent, + "parentfield": "courses", + "parenttype": "LMS Batch", + } + ) + doc.save() + + return doc.name diff --git a/lms/lms/doctype/lms_class/test_lms_class.py b/lms/lms/doctype/lms_batch/test_lms_batch.py similarity index 100% rename from lms/lms/doctype/lms_class/test_lms_class.py rename to lms/lms/doctype/lms_batch/test_lms_batch.py diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.json b/lms/lms/doctype/lms_certificate/lms_certificate.json index 512287bd..8033cd8f 100644 --- a/lms/lms/doctype/lms_certificate/lms_certificate.json +++ b/lms/lms/doctype/lms_certificate/lms_certificate.json @@ -8,10 +8,11 @@ "course", "member", "member_name", + "published", "column_break_3", "issue_date", "expiry_date", - "class_name" + "batch_name" ], "fields": [ { @@ -55,16 +56,22 @@ "read_only": 1 }, { - "fieldname": "class_name", + "fieldname": "batch_name", "fieldtype": "Link", "in_standard_filter": 1, - "label": "Class Name", - "options": "LMS Class" + "label": "Batch", + "options": "LMS Batch" + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Publish on Participant Page" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-23 14:48:49.351394", + "modified": "2023-09-13 11:03:23.479255", "modified_by": "Administrator", "module": "LMS", "name": "LMS Certificate", @@ -81,6 +88,18 @@ "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 } ], "sort_field": "modified", 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 40753451..eb66ee71 100644 --- a/lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json +++ b/lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json @@ -13,7 +13,7 @@ "date", "start_time", "end_time", - "class_name", + "batch_name", "section_break_6", "rating", "status", @@ -97,16 +97,16 @@ "fieldtype": "Column Break" }, { - "fieldname": "class_name", + "fieldname": "batch_name", "fieldtype": "Link", "in_standard_filter": 1, - "label": "Class Name", - "options": "LMS Class" + "label": "Batch Name", + "options": "LMS Batch" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-23 14:51:21.947160", + "modified": "2023-08-23 14:51:21.947169", "modified_by": "Administrator", "module": "LMS", "name": "LMS Certificate Evaluation", diff --git a/lms/lms/doctype/lms_certificate_request/lms_certificate_request.json b/lms/lms/doctype/lms_certificate_request/lms_certificate_request.json index 957c9151..e8f35028 100644 --- a/lms/lms/doctype/lms_certificate_request/lms_certificate_request.json +++ b/lms/lms/doctype/lms_certificate_request/lms_certificate_request.json @@ -8,7 +8,7 @@ "field_order": [ "course", "evaluator", - "class_name", + "batch_name", "column_break_4", "member", "member_name", @@ -100,16 +100,16 @@ "read_only": 1 }, { - "fieldname": "class_name", + "fieldname": "batch_name", "fieldtype": "Link", "in_standard_filter": 1, - "label": "Class Name", - "options": "LMS Class" + "label": "Batch Name", + "options": "LMS Batch" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-23 14:50:37.618350", + "modified": "2023-08-23 14:50:37.618352", "modified_by": "Administrator", "module": "LMS", "name": "LMS Certificate Request", diff --git a/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py b/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py index c9307fec..6368bd79 100644 --- a/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py +++ b/lms/lms/doctype/lms_certificate_request/lms_certificate_request.py @@ -104,9 +104,7 @@ def update_meeting_details(eval, event, calendar): @frappe.whitelist() -def create_certificate_request( - course, date, day, start_time, end_time, class_name=None -): +def create_certificate_request(course, date, day, start_time, end_time, batch=None): is_member = frappe.db.exists( {"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user} ) @@ -117,13 +115,13 @@ def create_certificate_request( eval.update( { "course": course, - "evaluator": get_evaluator(course, class_name), + "evaluator": get_evaluator(course, batch), "member": frappe.session.user, "date": date, "day": day, "start_time": start_time, "end_time": end_time, - "class_name": class_name, + "batch": batch, } ) eval.save(ignore_permissions=True) diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 27384f3f..74ccc51a 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -5,12 +5,11 @@ import json import random import frappe from frappe.model.document import Document -from frappe.utils import cint, validate_phone_number +from frappe.utils import cint from frappe.utils.telemetry import capture from lms.lms.utils import get_chapters, can_create_courses from ...utils import generate_slug, validate_image from frappe import _ -import razorpay class LMSCourse(Document): @@ -364,115 +363,3 @@ def reorder_chapter(chapter_array): "idx": chapter_array.index(chap) + 1, }, ) - - -@frappe.whitelist() -def get_payment_options(course, phone): - validate_phone_number(phone, True) - course_details = frappe.db.get_value( - "LMS Course", course, ["name", "title", "currency", "course_price"], as_dict=True - ) - razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key") - client = get_client() - order = create_order(client, course_details) - - options = { - "key_id": razorpay_key, - "name": frappe.db.get_single_value("Website Settings", "app_name"), - "description": _("Payment for {0} course").format(course_details["title"]), - "order_id": order["id"], - "amount": order["amount"] * 100, - "currency": order["currency"], - "prefill": { - "name": frappe.db.get_value("User", frappe.session.user, "full_name"), - "email": frappe.session.user, - "contact": phone, - }, - } - return options - - -def save_address(address): - address = json.loads(address) - address.update( - { - "address_title": frappe.db.get_value("User", frappe.session.user, "full_name"), - "address_type": "Billing", - "is_primary_address": 1, - "email_id": frappe.session.user, - } - ) - doc = frappe.new_doc("Address") - doc.update(address) - doc.save(ignore_permissions=True) - return doc.name - - -def get_client(): - razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key") - razorpay_secret = frappe.db.get_single_value("LMS Settings", "razorpay_secret") - - if not razorpay_key and not razorpay_secret: - frappe.throw( - _( - "There is a problem with the payment gateway. Please contact the Administrator to proceed." - ) - ) - - return razorpay.Client(auth=(razorpay_key, razorpay_secret)) - - -def create_order(client, course_details): - try: - return client.order.create( - { - "amount": course_details.course_price * 100, - "currency": course_details.currency, - } - ) - except Exception as e: - frappe.throw( - _("Error during payment: {0}. Please contact the Administrator.").format(e) - ) - - -@frappe.whitelist() -def verify_payment(response, course, address, order_id): - response = json.loads(response) - client = get_client() - client.utility.verify_payment_signature( - { - "razorpay_order_id": order_id, - "razorpay_payment_id": response["razorpay_payment_id"], - "razorpay_signature": response["razorpay_signature"], - } - ) - - return create_membership(address, response, course, client) - - -def create_membership(address, response, course, client): - try: - address_name = save_address(address) - membership = frappe.new_doc("LMS Enrollment") - payment = client.payment.fetch(response["razorpay_payment_id"]) - - membership.update( - { - "member": frappe.session.user, - "course": course, - "address": address_name, - "payment_received": 1, - "order_id": response["razorpay_order_id"], - "payment_id": response["razorpay_payment_id"], - "amount": payment["amount"] / 100, - "currency": payment["currency"], - } - ) - membership.save(ignore_permissions=True) - - return f"/courses/{course}/learn/1.1" - except Exception as e: - frappe.throw( - _("Error during payment: {0}. Please contact the Administrator.").format(e) - ) diff --git a/lms/lms/doctype/lms_enrollment/lms_enrollment.json b/lms/lms/doctype/lms_enrollment/lms_enrollment.json index b346597c..2ebf9161 100644 --- a/lms/lms/doctype/lms_enrollment/lms_enrollment.json +++ b/lms/lms/doctype/lms_enrollment/lms_enrollment.json @@ -7,22 +7,15 @@ "field_order": [ "course", "member_type", - "batch_old", + "payment", "column_break_3", "member", "member_name", "member_username", - "billing_information_section", - "address", - "amount", - "currency", - "column_break_rvzn", - "order_id", - "payment_id", - "payment_received", "section_break_8", "cohort", "subgroup", + "batch_old", "column_break_12", "current_lesson", "progress", @@ -122,52 +115,15 @@ "fieldtype": "Section Break" }, { - "fieldname": "billing_information_section", - "fieldtype": "Section Break", - "label": "Billing Information" - }, - { - "fieldname": "address", + "fieldname": "payment", "fieldtype": "Link", - "label": "Address", - "options": "Address" - }, - { - "default": "0", - "fieldname": "payment_received", - "fieldtype": "Check", - "label": "Payment Received" - }, - { - "fieldname": "column_break_rvzn", - "fieldtype": "Column Break" - }, - { - "fieldname": "order_id", - "fieldtype": "Data", - "label": "Order ID" - }, - { - "fieldname": "payment_id", - "fieldtype": "Data", - "label": "Payment ID" - }, - { - "fieldname": "amount", - "fieldtype": "Data", - "label": "Amount", - "options": "currency" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "label": "Currency", - "options": "Currency" + "label": "Payment", + "options": "LMS Payment" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-17 13:52:49.450491", + "modified": "2023-08-24 17:52:35.487141", "modified_by": "Administrator", "module": "LMS", "name": "LMS Enrollment", 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 814bc662..1e68dbfc 100644 --- a/lms/lms/doctype/lms_live_class/lms_live_class.json +++ b/lms/lms/doctype/lms_live_class/lms_live_class.json @@ -9,7 +9,7 @@ "field_order": [ "title", "host", - "class_name", + "batch_name", "password", "auto_recording", "column_break_astv", @@ -111,10 +111,10 @@ "reqd": 1 }, { - "fieldname": "class_name", + "fieldname": "batch_name", "fieldtype": "Link", - "label": "Class", - "options": "LMS Class" + "label": "Batch", + "options": "LMS Batch" }, { "default": "No Recording", @@ -126,7 +126,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-03-14 18:44:48.813102", + "modified": "2023-03-14 18:44:48.813103", "modified_by": "Administrator", "module": "LMS", "name": "LMS Live Class", diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.py b/lms/lms/doctype/lms_live_class/lms_live_class.py index 24d239db..872f2a06 100644 --- a/lms/lms/doctype/lms_live_class/lms_live_class.py +++ b/lms/lms/doctype/lms_live_class/lms_live_class.py @@ -34,7 +34,7 @@ class LMSLiveClass(Document): def add_event_participants(self, event, calendar): participants = frappe.get_all( - "Class Student", {"parent": self.class_name}, pluck="student" + "Batch Student", {"parent": self.class_name}, pluck="student" ) participants.append(frappe.session.user) diff --git a/lms/www/classes/__init__.py b/lms/lms/doctype/lms_payment/__init__.py similarity index 100% rename from lms/www/classes/__init__.py rename to lms/lms/doctype/lms_payment/__init__.py diff --git a/lms/lms/doctype/lms_payment/lms_payment.js b/lms/lms/doctype/lms_payment/lms_payment.js new file mode 100644 index 00000000..11d42cab --- /dev/null +++ b/lms/lms/doctype/lms_payment/lms_payment.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("LMS Payment", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json new file mode 100644 index 00000000..696d5af9 --- /dev/null +++ b/lms/lms/doctype/lms_payment/lms_payment.json @@ -0,0 +1,147 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:PAY-{#####}", + "creation": "2023-08-24 17:46:52.065763", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "member", + "column_break_rqkd", + "billing_name", + "payment_received", + "payment_details_section", + "currency", + "amount", + "amount_with_gst", + "column_break_yxpl", + "order_id", + "payment_id", + "billing_details_section", + "address", + "column_break_monu", + "gstin", + "pan" + ], + "fields": [ + { + "fieldname": "order_id", + "fieldtype": "Data", + "label": "Order ID" + }, + { + "fieldname": "payment_id", + "fieldtype": "Data", + "label": "Payment ID" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "column_break_rqkd", + "fieldtype": "Column Break" + }, + { + "fieldname": "gstin", + "fieldtype": "Data", + "label": "GSTIN" + }, + { + "fieldname": "pan", + "fieldtype": "Data", + "label": "PAN" + }, + { + "fieldname": "address", + "fieldtype": "Link", + "label": "Address", + "options": "Address" + }, + { + "default": "0", + "fieldname": "payment_received", + "fieldtype": "Check", + "label": "Payment Received" + }, + { + "fetch_from": "user.full_name", + "fieldname": "billing_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Billing Name", + "reqd": 1 + }, + { + "fieldname": "payment_details_section", + "fieldtype": "Section Break", + "label": "Payment Details" + }, + { + "fieldname": "column_break_yxpl", + "fieldtype": "Column Break" + }, + { + "fieldname": "billing_details_section", + "fieldtype": "Section Break", + "label": "Billing Details" + }, + { + "fieldname": "column_break_monu", + "fieldtype": "Column Break" + }, + { + "fieldname": "member", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Member", + "options": "User", + "reqd": 1 + }, + { + "depends_on": "eval:doc.currency == \"INR\";", + "fieldname": "amount_with_gst", + "fieldtype": "Currency", + "label": "Amount with GST" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-09-12 10:40:22.721371", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Payment", + "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 + } + ], + "show_title_field_in_link": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "billing_name" +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_payment/lms_payment.py b/lms/lms/doctype/lms_payment/lms_payment.py new file mode 100644 index 00000000..a250fd99 --- /dev/null +++ b/lms/lms/doctype/lms_payment/lms_payment.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 LMSPayment(Document): + pass diff --git a/lms/lms/doctype/lms_payment/test_lms_payment.py b/lms/lms/doctype/lms_payment/test_lms_payment.py new file mode 100644 index 00000000..3589140d --- /dev/null +++ b/lms/lms/doctype/lms_payment/test_lms_payment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSPayment(FrappeTestCase): + pass diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index 4c8b4d35..36741a4b 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -20,9 +20,13 @@ "allow_student_progress", "payment_section", "razorpay_key", - "default_currency", - "column_break_cfcv", "razorpay_secret", + "apply_gst", + "column_break_cfcv", + "default_currency", + "show_usd_equivalent", + "apply_rounding", + "exception_country", "signup_settings_tab", "signup_settings_section", "terms_of_use", @@ -188,6 +192,7 @@ "default": "0", "fieldname": "allow_student_progress", "fieldtype": "Check", + "hidden": 1, "label": "Allow students to see each others progress in class" }, { @@ -223,12 +228,37 @@ "fieldname": "razorpay_secret", "fieldtype": "Password", "label": "Razorpay Secret" + }, + { + "default": "0", + "fieldname": "apply_gst", + "fieldtype": "Check", + "label": "Apply GST for India" + }, + { + "default": "0", + "fieldname": "show_usd_equivalent", + "fieldtype": "Check", + "label": "Show USD Equivalent" + }, + { + "depends_on": "show_usd_equivalent", + "fieldname": "exception_country", + "fieldtype": "Table MultiSelect", + "label": "Maintain Original Currency", + "options": "Payment Country" + }, + { + "default": "0", + "fieldname": "apply_rounding", + "fieldtype": "Check", + "label": "Apply Rounding on Equivalent" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-02 18:59:01.267732", + "modified": "2023-09-11 21:56:39.996898", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", diff --git a/lms/lms/doctype/payment_country/__init__.py b/lms/lms/doctype/payment_country/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/payment_country/payment_country.js b/lms/lms/doctype/payment_country/payment_country.js new file mode 100644 index 00000000..3ad2f61b --- /dev/null +++ b/lms/lms/doctype/payment_country/payment_country.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Payment Country", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/payment_country/payment_country.json b/lms/lms/doctype/payment_country/payment_country.json new file mode 100644 index 00000000..cf493409 --- /dev/null +++ b/lms/lms/doctype/payment_country/payment_country.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-09-11 11:53:16.253740", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "country" + ], + "fields": [ + { + "fieldname": "country", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Country", + "options": "Country" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-09-11 12:04:56.048632", + "modified_by": "Administrator", + "module": "LMS", + "name": "Payment Country", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/payment_country/payment_country.py b/lms/lms/doctype/payment_country/payment_country.py new file mode 100644 index 00000000..9d834847 --- /dev/null +++ b/lms/lms/doctype/payment_country/payment_country.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PaymentCountry(Document): + pass diff --git a/lms/lms/doctype/payment_country/test_payment_country.py b/lms/lms/doctype/payment_country/test_payment_country.py new file mode 100644 index 00000000..994c246d --- /dev/null +++ b/lms/lms/doctype/payment_country/test_payment_country.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymentCountry(FrappeTestCase): + pass diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 2829f5d6..ec3e4489 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -1,6 +1,10 @@ import re import string import frappe +import json +import razorpay +import requests +import base64 from frappe import _ from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result from frappe.desk.doctype.notification_log.notification_log import make_notification_logs @@ -13,9 +17,9 @@ from frappe.utils import ( format_date, get_datetime, getdate, + validate_phone_number, ) from frappe.utils.dateutils import get_period - from lms.lms.md import find_macros, markdown_to_html RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+") @@ -796,13 +800,13 @@ def has_graded_assessment(submission): return False if status == "Not Graded" else True -def get_evaluator(course, class_name=None): +def get_evaluator(course, batch=None): evaluator = None - if class_name: + if batch: evaluator = frappe.db.get_value( - "Class Course", - {"parent": class_name, "course": course}, + "Batch Course", + {"parent": batch, "course": course}, "evaluator", ) @@ -828,3 +832,233 @@ def get_upcoming_evals(student, courses): evals.course_title = frappe.db.get_value("LMS Course", evals.course, "title") evals.evaluator_name = frappe.db.get_value("User", evals.evaluator, "full_name") return upcoming_evals + + +@frappe.whitelist() +def get_payment_options(doctype, docname, phone, country): + if not frappe.db.exists(doctype, docname): + frappe.throw(_("Invalid document provided.")) + + validate_phone_number(phone, True) + details = get_details(doctype, docname) + details.amount, details.currency = check_multicurrency( + details.amount, details.currency + ) + if details.currency == "INR": + details.amount, details.gst_applied = apply_gst(details.amount, country) + + client = get_client() + order = create_order(client, details.amount, details.currency) + + options = { + "key_id": frappe.db.get_single_value("LMS Settings", "razorpay_key"), + "name": frappe.db.get_single_value("Website Settings", "app_name"), + "description": _("Payment for {0} course").format(details["title"]), + "order_id": order["id"], + "amount": order["amount"] * 100, + "currency": order["currency"], + "prefill": { + "name": frappe.db.get_value("User", frappe.session.user, "full_name"), + "email": frappe.session.user, + "contact": phone, + }, + } + return options + + +def check_multicurrency(amount, currency): + show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent") + exception_country = frappe.get_all( + "Payment Country", filters={"parent": "LMS Settings"}, pluck="country" + ) + apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding") + country = frappe.db.get_value("User", frappe.session.user, "country") + + if not show_usd_equivalent or currency == "USD": + return amount, currency + + if exception_country and country in exception_country: + return amount, currency + + exchange_rate = get_current_exchange_rate(currency, "USD") + amount = amount * exchange_rate + currency = "USD" + + if apply_rounding and amount % 100 != 0: + amount = amount + 100 - amount % 100 + + return amount, currency + + +def apply_gst(amount, country=None): + gst_applied = False + apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst") + + if not country: + country = frappe.db.get_value("User", frappe.session.user, "country") + + if apply_gst and country == "India": + gst_applied = True + amount = amount * 1.18 + + return amount, gst_applied + + +def get_details(doctype, docname): + if doctype == "LMS Course": + details = frappe.db.get_value( + "LMS Course", + docname, + ["name", "title", "paid_course", "currency", "course_price as amount"], + as_dict=True, + ) + if not details.paid_course: + frappe.throw(_("This course is free.")) + else: + details = frappe.db.get_value( + "LMS Batch", + docname, + ["name", "title", "paid_batch", "currency", "amount"], + as_dict=True, + ) + if not details.paid_batch: + frappe.throw(_("To join this batch, please contact the Administrator.")) + + return details + + +def save_address(address): + address.update( + { + "address_title": frappe.db.get_value("User", frappe.session.user, "full_name"), + "address_type": "Billing", + "is_primary_address": 1, + "email_id": frappe.session.user, + } + ) + doc = frappe.new_doc("Address") + doc.update(address) + doc.save(ignore_permissions=True) + return doc.name + + +def get_client(): + settings = frappe.get_single("LMS Settings") + razorpay_key = settings.razorpay_key + razorpay_secret = settings.get_password("razorpay_secret", raise_exception=True) + + if not razorpay_key and not razorpay_secret: + frappe.throw( + _( + "There is a problem with the payment gateway. Please contact the Administrator to proceed." + ) + ) + + return razorpay.Client(auth=(razorpay_key, razorpay_secret)) + + +def create_order(client, amount, currency): + try: + return client.order.create( + { + "amount": amount * 100, + "currency": currency, + } + ) + except Exception as e: + frappe.throw( + _("Error during payment: {0}. Please contact the Administrator.").format(e) + ) + + +@frappe.whitelist() +def verify_payment(response, doctype, docname, address, order_id): + response = json.loads(response) + client = get_client() + client.utility.verify_payment_signature( + { + "razorpay_order_id": order_id, + "razorpay_payment_id": response["razorpay_payment_id"], + "razorpay_signature": response["razorpay_signature"], + } + ) + + payment = record_payment(address, response, client, doctype, docname) + if doctype == "LMS Course": + return create_membership(docname, payment) + else: + return add_student_to_batch(docname, payment) + + +def record_payment(address, response, client, doctype, docname): + address = frappe._dict(json.loads(address)) + address_name = save_address(address) + + payment_details = get_payment_details(doctype, docname, address) + payment_doc = frappe.new_doc("LMS Payment") + payment_doc.update( + { + "member": frappe.session.user, + "billing_name": address.billing_name, + "address": address_name, + "payment_received": 1, + "order_id": response["razorpay_order_id"], + "payment_id": response["razorpay_payment_id"], + "amount": payment_details["amount"], + "currency": payment_details["currency"], + "amount_with_gst": payment_details["amount_with_gst"], + "gstin": address.gstin, + "pan": address.pan, + } + ) + payment_doc.save(ignore_permissions=True) + return payment_doc.name + + +def get_payment_details(doctype, docname, address): + amount_field = "course_price" if doctype == "LMS Course" else "amount" + amount = frappe.db.get_value(doctype, docname, amount_field) + currency = frappe.db.get_value(doctype, docname, "currency") + amount_with_gst = 0 + + amount, currency = check_multicurrency(amount, currency) + if currency == "INR" and address.country == "India": + amount_with_gst, gst_applied = apply_gst(amount, address.country) + + return { + "amount": amount, + "currency": currency, + "amount_with_gst": amount_with_gst, + } + + +def create_membership(course, payment): + membership = frappe.new_doc("LMS Enrollment") + membership.update( + {"member": frappe.session.user, "course": course, "payment": payment} + ) + membership.save(ignore_permissions=True) + return f"/courses/{course}/learn/1.1" + + +def add_student_to_batch(batchname, payment): + student = frappe.new_doc("Batch Student") + student.update( + { + "student": frappe.session.user, + "payment": payment, + "parent": batchname, + "parenttype": "LMS Batch", + "parentfield": "students", + } + ) + student.save(ignore_permissions=True) + return f"/batches/{batchname}" + + +def get_current_exchange_rate(source, target="USD"): + url = f"https://api.frankfurter.app/latest?from={source}&to={target}" + + response = requests.request("GET", url) + details = response.json() + return details["rates"][target] diff --git a/lms/lms/web_form/evaluation/evaluation.js b/lms/lms/web_form/evaluation/evaluation.js index 3b0372f4..3214f8b6 100644 --- a/lms/lms/web_form/evaluation/evaluation.js +++ b/lms/lms/web_form/evaluation/evaluation.js @@ -3,7 +3,7 @@ frappe.ready(function () { let data = frappe.web_form.get_values(); if (data.class) { setTimeout(() => { - window.location.href = `/classes/${data.class}`; + window.location.href = `/batches/${data.class}`; }, 2000); } }; diff --git a/lms/lms/web_form/evaluation/evaluation.json b/lms/lms/web_form/evaluation/evaluation.json index c2dd3e12..6dbafdae 100644 --- a/lms/lms/web_form/evaluation/evaluation.json +++ b/lms/lms/web_form/evaluation/evaluation.json @@ -22,7 +22,7 @@ "login_required": 1, "max_attachment_size": 0, "meta_image": "/files/og_image_web_form_evaluation_68ddf18e.png", - "modified": "2023-08-23 14:37:03.086303", + "modified": "2023-08-23 14:37:03.086304", "modified_by": "Administrator", "module": "LMS", "name": "evaluation", @@ -63,13 +63,13 @@ }, { "allow_read_on_all_link_options": 0, - "fieldname": "class_name", + "fieldname": "batch_name", "fieldtype": "Link", "hidden": 0, - "label": "Class Name", + "label": "Batch Name", "max_length": 0, "max_value": 0, - "options": "LMS Class", + "options": "LMS Batch", "read_only": 0, "reqd": 0, "show_in_filter": 0 diff --git a/lms/patches.txt b/lms/patches.txt index d2f8efef..19f9a649 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -64,4 +64,8 @@ execute:frappe.permissions.reset_perms("LMS Certificate Evaluation") lms.patches.v1_0.paid_certificate_to_paid_course #18-08-2023 lms.patches.v1_0.revert_class_registration #18-08-2023 lms.patches.v1_0.rename_lms_batch_doctype -lms.patches.v1_0.rename_lms_batch_membership_doctype \ No newline at end of file +lms.patches.v1_0.rename_lms_batch_membership_doctype +lms.patches.v1_0.rename_lms_class_to_lms_batch +lms.patches.v1_0.rename_classes_in_navbar +lms.patches.v1_0.publish_batches +lms.patches.v1_0.publish_certificates \ No newline at end of file diff --git a/lms/patches/v1_0/publish_batches.py b/lms/patches/v1_0/publish_batches.py new file mode 100644 index 00000000..7b16c4f1 --- /dev/null +++ b/lms/patches/v1_0/publish_batches.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + frappe.reload_doc("lms", "doctype", "lms_batch") + batches = frappe.get_all("LMS Batch", pluck="name") + + for batch in batches: + frappe.db.set_value("LMS Batch", batch, "Published", 1) diff --git a/lms/patches/v1_0/publish_certificates.py b/lms/patches/v1_0/publish_certificates.py new file mode 100644 index 00000000..608f0d54 --- /dev/null +++ b/lms/patches/v1_0/publish_certificates.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + frappe.reload_doc("lms", "doctype", "lms_certificate") + certificates = frappe.get_all("LMS Certificate", pluck="name") + + for certificate in certificates: + frappe.db.set_value("LMS Certificate", certificate, "published", 1) diff --git a/lms/patches/v1_0/rename_classes_in_navbar.py b/lms/patches/v1_0/rename_classes_in_navbar.py new file mode 100644 index 00000000..133533a4 --- /dev/null +++ b/lms/patches/v1_0/rename_classes_in_navbar.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + frappe.db.set_value( + "Top Bar Item", + {"url": "/classes"}, + { + "label": "Batches", + "url": "/batches", + }, + ) diff --git a/lms/patches/v1_0/rename_lms_class_to_lms_batch.py b/lms/patches/v1_0/rename_lms_class_to_lms_batch.py new file mode 100644 index 00000000..333cde40 --- /dev/null +++ b/lms/patches/v1_0/rename_lms_class_to_lms_batch.py @@ -0,0 +1,32 @@ +import frappe +from frappe.model.rename_doc import rename_doc + + +def execute(): + if frappe.db.exists("DocField", {"fieldname": "students", "parent": "LMS Batch"}): + return + + rename_lms_class() + rename_class_student() + rename_class_courses() + + +def rename_lms_class(): + frappe.flags.ignore_route_conflict_validation = True + rename_doc("DocType", "LMS Class", "LMS Batch") + frappe.flags.ignore_route_conflict_validation = False + frappe.reload_doctype("LMS Batch", force=True) + + +def rename_class_student(): + frappe.flags.ignore_route_conflict_validation = True + rename_doc("DocType", "Class Student", "Batch Student") + frappe.flags.ignore_route_conflict_validation = False + frappe.reload_doctype("Batch Student", force=True) + + +def rename_class_courses(): + frappe.flags.ignore_route_conflict_validation = True + rename_doc("DocType", "Class Course", "Batch Course") + frappe.flags.ignore_route_conflict_validation = False + frappe.reload_doctype("Batch Course", force=True) diff --git a/lms/public/css/style.css b/lms/public/css/style.css index e5f5149f..e414f449 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -911,7 +911,7 @@ input[type=checkbox] { .profile-name-section { display: flex; align-items: center; - margin: 1rem 0 0.25rem; + margin: 0.5rem 0 0.25rem; } @media (max-width: 550px) { @@ -2188,25 +2188,12 @@ select { border-bottom: none; } -.awesomplete ul li:last-child { - display: none; -} - .students-parent { display: grid; grid-template-columns: repeat(auto-fill, 150px); grid-gap: 1rem; } -.btn-remove-course { - opacity: 0; - margin-top: 0.25rem; -} - -.btn-remove-course:hover { - opacity: 1; -} - .rows .grid-row .data-row, .rows .grid-row .grid-footer-toolbar, .grid-form-heading { @@ -2302,6 +2289,10 @@ select { padding-right: 1rem !important; } +.class-overlay { + top: 30%; +} + .course-list-menu { display: flex; align-items: center; @@ -2346,6 +2337,19 @@ select { margin-bottom: 1rem; } +.batch-course-list .cards-parent { + row-gap: 3rem +} + .embed-tool__caption { display: none; +} + +.card-buttons { + display: flex; + position: relative; + top: 10%; + left: 80%; + z-index: 10; + width: fit-content; } \ No newline at end of file diff --git a/lms/public/icons/symbol-defs.svg b/lms/public/icons/symbol-defs.svg index f7823137..102d9efe 100644 --- a/lms/public/icons/symbol-defs.svg +++ b/lms/public/icons/symbol-defs.svg @@ -104,11 +104,18 @@ + + + + + + + diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index 224bcb28..8da2d052 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -37,8 +37,8 @@ frappe.ready(() => { show_no_preview_dialog(e); }); - $("#create-class").click((e) => { - open_class_dialog(e); + $("#create-batch").click((e) => { + open_batch_dialog(e); }); $("#course-filter").change((e) => { @@ -250,37 +250,55 @@ const show_no_preview_dialog = (e) => { $("#no-preview-modal").modal("show"); }; -const open_class_dialog = () => { - this.class_dialog = new frappe.ui.Dialog({ - title: __("New Class"), +const open_batch_dialog = () => { + this.batch_dialog = new frappe.ui.Dialog({ + title: __("New Batch"), fields: [ { fieldtype: "Data", label: __("Title"), fieldname: "title", reqd: 1, - default: class_info && class_info.title, + default: batch_info && batch_info.title, + }, + { + fieldtype: "Check", + label: __("Published"), + fieldname: "published", + default: batch_info && batch_info.published, + }, + { + fieldtype: "Column Break", + }, + { + fieldtype: "Int", + label: __("Seat Count"), + fieldname: "seat_count", + default: batch_info && batch_info.seat_count, + }, + { + fieldtype: "Section Break", }, { fieldtype: "Date", label: __("Start Date"), fieldname: "start_date", reqd: 1, - default: class_info && class_info.start_date, + default: batch_info && batch_info.start_date, }, { fieldtype: "Date", label: __("End Date"), fieldname: "end_date", reqd: 1, - default: class_info && class_info.end_date, + default: batch_info && batch_info.end_date, }, { fieldtype: "Select", label: __("Medium"), fieldname: "medium", options: ["Online", "Offline"], - default: (class_info && class_info.medium) || "Online", + default: (batch_info && batch_info.medium) || "Online", }, { fieldtype: "Column Break", @@ -289,26 +307,20 @@ const open_class_dialog = () => { fieldtype: "Time", label: __("Start Time"), fieldname: "start_time", - default: class_info && class_info.start_time, + default: batch_info && batch_info.start_time, }, { fieldtype: "Time", label: __("End Time"), fieldname: "end_time", - default: class_info && class_info.end_time, - }, - { - fieldtype: "Int", - label: __("Seat Count"), - fieldname: "seat_count", - default: class_info && class_info.seat_count, + default: batch_info && batch_info.end_time, }, { fieldtype: "Link", label: __("Category"), fieldname: "category", options: "LMS Category", - default: class_info && class_info.category, + default: batch_info && batch_info.category, }, { fieldtype: "Section Break", @@ -317,43 +329,74 @@ const open_class_dialog = () => { fieldtype: "Small Text", label: __("Description"), fieldname: "description", - default: class_info && class_info.description, + default: batch_info && batch_info.description, reqd: 1, }, + { + fieldtype: "Text Editor", + label: __("Batch Details"), + fieldname: "batch_details", + default: batch_info && batch_info.batch_details, + reqd: 1, + }, + { + fieldtype: "Section Break", + label: __("Pricing"), + fieldname: "pricing", + }, + { + fieldtype: "Check", + label: __("Paid Batch"), + fieldname: "paid_batch", + default: batch_info && batch_info.paid_batch, + }, + { + fieldtype: "Currency", + label: __("Amount"), + fieldname: "amount", + default: batch_info && batch_info.amount, + mandatory_depends_on: "paid_batch", + depends_on: "paid_batch", + }, + { + fieldtype: "Link", + label: __("Currency"), + fieldname: "currency", + options: "Currency", + default: batch_info && batch_info.currency, + mandatory_depends_on: "paid_batch", + depends_on: "paid_batch", + only_select: 1, + }, ], primary_action_label: __("Save"), primary_action: (values) => { - save_class(values); + save_batch(values); }, }); - this.class_dialog.show(); + this.batch_dialog.show(); }; -const save_class = (values) => { +const save_batch = (values) => { + let args = {}; + if (batch_info) { + args = Object.assign(batch_info, values); + } else { + args = values; + } frappe.call({ - method: "lms.lms.doctype.lms_class.lms_class.create_class", - args: { - title: values.title, - start_date: values.start_date, - end_date: values.end_date, - description: values.description, - seat_count: values.seat_count, - start_time: values.start_time, - end_time: values.end_time, - medium: values.medium, - category: values.category, - name: class_info && class_info.name, - }, + method: "lms.lms.doctype.lms_batch.lms_batch.create_batch", + args: args, callback: (r) => { if (r.message) { frappe.show_alert({ - message: class_info - ? __("Class Updated") - : __("Class Created"), + message: batch_info + ? __("Batch Updated") + : __("Batch Created"), indicator: "green", }); - this.class_dialog.hide(); - window.location.href = `/classes/${r.message.name}`; + this.batch_dialog.hide(); + window.location.href = `/batches/details/${r.message.name}`; } }, }); diff --git a/lms/templates/certificates_section.html b/lms/templates/certificates_section.html index 20f02149..311d3ca6 100644 --- a/lms/templates/certificates_section.html +++ b/lms/templates/certificates_section.html @@ -6,7 +6,7 @@ {% set course = frappe.db.get_value("LMS Course", certificate.course, ["title", "name", "image"], as_dict=True) %}
-
+
{{ course.title }}
diff --git a/lms/www/assignment_submission/assignment_submission.html b/lms/www/assignment_submission/assignment_submission.html index a645b0a0..3a25ee2c 100644 --- a/lms/www/assignment_submission/assignment_submission.html +++ b/lms/www/assignment_submission/assignment_submission.html @@ -29,8 +29,8 @@ {% endif %}
- - {{ _("All Classes") }} + + {{ _("All Batches") }} {{ _("Assignment Submission") }} @@ -52,13 +52,18 @@ {% macro SubmissionForm(assignment) %}
- {% if submission.name and is_moderator %} -
-
- {{ _("Student Name") }} + {% if submission.name %} +
+ {{ _("You've successfully submitted the assignment. Once the moderator grades your submission, you'll find the details here. Feel free to make edits to your submission if needed.") }}
- {{ submission.member_name }} -
+ {% if is_moderator %} +
+
+ {{ _("Student Name") }} +
+ {{ submission.member_name }} +
+ {% endif %} {% endif %}
@@ -81,14 +86,15 @@
{{ _("Browse").format(assignment.type) }}
- {% else %} diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 7ad0b37b..641f5256 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -116,15 +116,6 @@ {%- block script %} {{ super() }} - {% if is_moderator %} - - {% endif %} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index d0efee8b..833649fd 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -1,6 +1,5 @@ frappe.ready(() => { let self = this; - this.quiz_in_lesson = []; frappe.telemetry.capture("on_lesson_creation_page", "lms"); @@ -15,7 +14,6 @@ frappe.ready(() => { } setup_editor(); - fetch_quiz_list(); $("#save-lesson").click((e) => { save_lesson(e); @@ -90,11 +88,10 @@ const parse_string_to_lesson = () => { }); } else if (block.includes("{{ Quiz")) { let quiz = block.match(/'([^']+)'/)[1]; - this.quiz_in_lesson.push(quiz); lesson_blocks.push({ type: "quiz", data: { - quiz: [quiz], + quiz: quiz, }, }); } else if (block.includes("{{ Video")) { @@ -156,9 +153,7 @@ const parse_lesson_to_string = (data) => { if (block.type == "youtube") { lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`; } else if (block.type == "quiz") { - block.data.quiz.forEach((quiz) => { - lesson_content += `{{ Quiz("${quiz}") }}\n`; - }); + lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`; } else if (block.type == "upload") { let url = block.data.file_url; lesson_content += block.data.is_video @@ -233,15 +228,6 @@ const validate_mandatory = (lesson_content) => { } }; -const fetch_quiz_list = () => { - frappe.call({ - method: "lms.lms.doctype.lms_quiz.lms_quiz.get_user_quizzes", - callback: (r) => { - self.quiz_list = r.message; - }, - }); -}; - const is_video = (url) => { let video_types = ["mov", "mp4", "mkv"]; let video_extension = url.split(".").pop(); @@ -339,57 +325,10 @@ class Quiz { this.data = data; } - get_fields() { - let fields = [ - { - fieldname: "start_section", - fieldtype: "Section Break", - label: __( - "To create a new quiz, click on the button below. Once you have created the new quiz you can come back to this lesson and add it from here." - ), - }, - { - fieldname: "create_quiz", - fieldtype: "Button", - label: __("Create Quiz"), - click: () => { - window.location.href = "/quizzes"; - }, - }, - { - fieldname: "quiz_information", - fieldtype: "HTML", - options: __("OR"), - }, - { - fieldname: "quiz_list_section", - fieldtype: "Section Break", - label: __("Select a exisitng quiz to add to this lesson."), - }, - ]; - let break_index = Math.ceil(self.quiz_list.length / 2) + 4; - - self.quiz_list.forEach((quiz) => { - fields.push({ - fieldname: quiz.name, - fieldtype: "Check", - label: quiz.title, - default: self.quiz_in_lesson.includes(quiz.name) ? 1 : 0, - read_only: self.quiz_in_lesson.includes(quiz.name) ? 1 : 0, - }); - }); - - fields.splice(break_index, 0, { - fieldname: "column_break", - fieldtype: "Column Break", - }); - return fields; - } - render() { this.wrapper = document.createElement("div"); if (this.data && this.data.quiz) { - $(this.wrapper).html(this.render_quiz()); + $(this.wrapper).html(this.render_quiz(this.data.quiz)); } else { this.render_quiz_dialog(); } @@ -398,16 +337,24 @@ class Quiz { render_quiz_dialog() { let me = this; - let fields = this.get_fields(); let quizdialog = new frappe.ui.Dialog({ title: __("Manage Quiz"), - fields: fields, + fields: [ + { + fieldname: "quiz", + fieldtype: "Link", + label: __("Quiz"), + options: "LMS Quiz", + only_select: 1, + }, + ], primary_action_label: __("Insert"), primary_action(values) { - me.analyze_quiz_list(values); + me.quiz = values.quiz; quizdialog.hide(); + $(me.wrapper).html(me.render_quiz(me.quiz)); }, - secondary_action_label: __("Create New Quiz"), + secondary_action_label: __("Create New"), secondary_action: () => { window.location.href = `/quizzes`; }, @@ -419,38 +366,19 @@ class Quiz { }, 1000); } - analyze_quiz_list(values) { - /* If quiz is selected and is not already in the lesson then render it.*/ - - this.quiz_to_render = []; - Object.keys(values).forEach((key) => { - if (values[key] === 1 && !self.quiz_in_lesson.includes(key)) { - self.quiz_in_lesson.push(key); - this.quiz_to_render.push(key); - } - }); - - $(this.wrapper).html(this.render_quiz()); - } - - render_quiz() { - let html = ``; - let quiz_list = this.data.quiz || this.quiz_to_render; - quiz_list.forEach((quiz) => { - html += `
- Quiz: ${quiz} -
`; - }); - return html; + render_quiz(quiz) { + return `
+ Quiz: ${quiz} +
`; } validate(savedData) { - return !savedData.quiz || !savedData.quiz.length ? false : true; + return !savedData.quiz || !savedData.quiz.trim() ? false : true; } save(block_content) { return { - quiz: this.data.quiz || this.quiz_to_render, + quiz: this.data.quiz || this.quiz, }; } } diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 743a01fc..d2d778ff 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -61,10 +61,10 @@
- {% if class_info.custom_component %} - {{ class_info.custom_component }} + {% if batch_info.custom_component %} + {{ batch_info.custom_component }} {% endif %}
{% endmacro %} - -{% macro ClassSections(class_info, class_courses, class_students, flow) %} +{% macro BatchSections(batch_info, batch_courses, batch_students, flow) %} {% endif %} + {% if is_moderator %}
- {{ StudentsSection(class_info, class_students) }} + {{ StudentsSection(batch_info, batch_students) }}
- {% if is_moderator %}
- {{ AssessmentsSection(class_info) }} + {{ AssessmentsSection(batch_info) }}
{% endif %} - {% if class_students | length and (is_moderator or is_student or is_evaluator) %} + {% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
- {{ Discussions(class_info) }} + {{ Discussions(batch_info) }}
- {{ LiveClassSection(class_info, live_classes) }} + {{ LiveClassSection(batch_info, live_classes) }}
{% endif %} @@ -204,7 +199,7 @@
{% endmacro %} -{% macro Dashboard(class_info, class_courses, current_student) %} +{% macro Dashboard(batch_info, batch_courses, current_student) %} {% set upcoming_evals = current_student.upcoming_evals %} {% set assessments = current_student.assessments %} @@ -224,46 +219,36 @@
{% endmacro %} -{% macro Discussions(class_info) %} +{% macro Discussions(batch_info) %}
{% set condition = is_moderator or is_student or is_evaluator %} - {% set doctype, docname = _("LMS Class"), class_info.name %} + {% set doctype, docname = _("LMS Batch"), batch_info.name %} {% set single_thread = True %} {% set title = "Discussions" %} {% set cta_title = "Post" %} {% set button_name = _("Start Learning") %} - {% set redirect_to = "/classes/" + class_info.name %} + {% set redirect_to = "/batches/" + batch_info.name %} {% set empty_state_title = _("Have a doubt?") %} {% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %} {% include "frappe/templates/discussions/discussions_section.html" %}
{% endmacro %} -{% macro CoursesSection(class_info, class_courses) %} +{% macro CoursesSection(batch_info, batch_courses) %}
{{ _("Courses") }}
- {% if is_moderator %} - - {% endif %}
- {% if class_courses | length %} + {% if batch_courses | length %}
- {% for course in class_courses %} + {% for course in batch_courses %}
{{ widgets.CourseCard(course=course, read_only=False) }} -
{% endfor %} @@ -278,7 +263,7 @@ {% endmacro %} -{% macro StudentsSection(class_info, class_students) %} +{% macro StudentsSection(batch_info, batch_students) %}
@@ -293,7 +278,7 @@
- {% if class_students | length %} + {% if batch_students | length %}
@@ -323,11 +308,11 @@
- {% for student in class_students %} + {% for student in batch_students %} {% set allow_progress = is_moderator or is_evaluator %}
- + {{ student.student_name }}
@@ -360,7 +345,7 @@ {% endmacro %} -{% macro AssessmentsSection(class_info) %} +{% macro AssessmentsSection(batch_info) %}
@@ -425,7 +410,7 @@ {% endmacro %} -{% macro LiveClassSection(class_info, live_classes) %} +{% macro LiveClassSection(batch_info, live_classes) %}
@@ -437,13 +422,13 @@ {% endif %}
- {{ CreateLiveClass(class_info) }} - {{ LiveClassList(class_info, live_classes) }} + {{ CreateLiveClass(batch_info) }} + {{ LiveClassList(batch_info, live_classes) }}
{% endmacro %} -{% macro CreateLiveClass(class_info) %} +{% macro CreateLiveClass(batch_info) %} {% if is_moderator %} {% endmacro %} -{% macro CourseDetails() %} +{% macro Details() %}
- {{ _("Course Name: ") }} {{ course.title }} + {% set label = "Course" if module == "course" else "Batch" %} + {{ _(label) }} : {{ title }}
- {{ _("Total Price: ") }} {{ frappe.utils.fmt_money(course.course_price, 2, course.currency) }} + {{ _("Total Price: ") }} {{ frappe.utils.fmt_money(amount, 2, currency) }}
+ {% if gst_applied %} + + {{ _("18% GST included") }} + + {% endif %}
{% endmacro %} @@ -49,7 +55,7 @@ {{ _("Billing Details") }}
-
@@ -58,11 +64,4 @@ {%- block script %} {{ super() }} - {% endblock %} diff --git a/lms/www/billing/billing.js b/lms/www/billing/billing.js index 6d2cf41f..1bb2e24f 100644 --- a/lms/www/billing/billing.js +++ b/lms/www/billing/billing.js @@ -1,6 +1,8 @@ frappe.ready(() => { if ($("#billing-form").length) { - setup_billing(); + frappe.require("controls.bundle.js", () => { + setup_billing(); + }); } $(".btn-pay").click((e) => { @@ -11,6 +13,12 @@ frappe.ready(() => { const setup_billing = () => { this.billing = new frappe.ui.FieldGroup({ fields: [ + { + fieldtype: "Data", + label: __("Billing Name"), + fieldname: "billing_name", + reqd: 1, + }, { fieldtype: "Data", label: __("Address Line 1"), @@ -28,20 +36,21 @@ const setup_billing = () => { fieldname: "city", reqd: 1, }, + { + fieldtype: "Column Break", + }, { fieldtype: "Data", label: __("State/Province"), fieldname: "state", }, - { - fieldtype: "Column Break", - }, { fieldtype: "Link", label: __("Country"), fieldname: "country", options: "Country", reqd: 1, + only_select: 1, }, { fieldtype: "Data", @@ -55,6 +64,26 @@ const setup_billing = () => { fieldname: "phone", reqd: 1, }, + { + fieldtype: "Section Break", + label: __("GST Details"), + fieldname: "gst_details", + depends_on: "eval:doc.country === 'India'", + }, + { + fieldtype: "Data", + label: __("GSTIN"), + fieldname: "gstin", + }, + { + fieldtype: "Column Break", + fieldname: "gst_details_break", + }, + { + fieldtype: "Data", + fieldname: "pan", + label: __("PAN"), + }, ], body: $("#billing-form").get(0), }); @@ -66,19 +95,23 @@ const setup_billing = () => { const generate_payment_link = (e) => { address = this.billing.get_values(); - let course = decodeURIComponent($(e.currentTarget).attr("data-course")); + let doctype = $(e.currentTarget).attr("data-doctype"); + let docname = decodeURIComponent($(e.currentTarget).attr("data-name")); frappe.call({ - method: "lms.lms.doctype.lms_course.lms_course.get_payment_options", + method: "lms.lms.utils.get_payment_options", args: { - course: course, + doctype: doctype, + docname: docname, phone: address.phone, + country: address.country, }, callback: (data) => { data.message.handler = (response) => { handle_success( response, - course, + doctype, + docname, address, data.message.order_id ); @@ -89,12 +122,13 @@ const generate_payment_link = (e) => { }); }; -const handle_success = (response, course, address, order_id) => { +const handle_success = (response, doctype, docname, address, order_id) => { frappe.call({ - method: "lms.lms.doctype.lms_course.lms_course.verify_payment", + method: "lms.lms.utils.verify_payment", args: { response: response, - course: course, + doctype: doctype, + docname: docname, address: address, order_id: order_id, }, diff --git a/lms/www/billing/billing.py b/lms/www/billing/billing.py index f1c0f871..3c5558ce 100644 --- a/lms/www/billing/billing.py +++ b/lms/www/billing/billing.py @@ -1,23 +1,77 @@ import frappe from frappe import _ +from lms.lms.utils import check_multicurrency, apply_gst def get_context(context): - course_name = frappe.form_dict.course + module = frappe.form_dict.module + docname = frappe.form_dict.modulename + doctype = "LMS Course" if module == "course" else "LMS Batch" - if not course_name: - raise ValueError(_("Course is required.")) + context.module = module + context.docname = docname + context.doctype = doctype + validate_access(doctype, docname, module) + get_billing_details(context) + context.amount, context.currency = check_multicurrency( + context.amount, context.currency + ) + + if context.currency == "INR": + context.amount, context.gst_applied = apply_gst(context.amount, None) + + +def validate_access(doctype, docname, module): if frappe.session.user == "Guest": raise frappe.PermissionError(_("You are not allowed to access this page.")) - membership = frappe.db.exists( - "LMS Enrollment", {"member": frappe.session.user, "course": course_name} - ) + if module not in ["course", "batch"]: + raise ValueError(_("Module is incorrect.")) - if membership: - raise frappe.PermissionError(_("You are already enrolled for this course")) + if not frappe.db.exists(doctype, docname): + raise ValueError(_("Module Name is incorrect or does not exist.")) - context.course = frappe.db.get_value( - "LMS Course", course_name, ["title", "name", "course_price", "currency"], as_dict=True - ) + if doctype == "LMS Course": + membership = frappe.db.exists( + "LMS Enrollment", {"member": frappe.session.user, "course": docname} + ) + if membership: + raise frappe.PermissionError(_("You are already enrolled for this course")) + + else: + membership = frappe.db.exists( + "Batch Student", {"student": frappe.session.user, "parent": docname} + ) + if membership: + raise frappe.PermissionError(_("You are already enrolled for this batch.")) + + +def get_billing_details(context): + if context.doctype == "LMS Course": + details = frappe.db.get_value( + "LMS Course", + context.docname, + ["title", "name", "paid_course", "course_price as amount", "currency"], + as_dict=True, + ) + + if not details.paid_course: + raise frappe.PermissionError(_("This course is free.")) + + else: + details = frappe.db.get_value( + "LMS Batch", + context.docname, + ["title", "name", "paid_batch", "amount", "currency"], + as_dict=True, + ) + + if not details.paid_batch: + raise frappe.PermissionError( + _("To join this batch, please contact the Administrator.") + ) + + context.title = details.title + context.amount = details.amount + context.currency = details.currency diff --git a/lms/www/certified_participants/certified_participants.html b/lms/www/certified_participants/certified_participants.html new file mode 100644 index 00000000..f4e49cb3 --- /dev/null +++ b/lms/www/certified_participants/certified_participants.html @@ -0,0 +1,63 @@ +{% extends "lms/templates/lms_base.html" %} +{% block title %} + {{ _("Certified Participants") }} +{% endblock %} + +{% block page_content %} +
+
+
+ {% if course_filter | length %} + + {% endif %} +
+ {{ _("Certified Participants") }} +
+
+ {% if participants | length %} + {{ ParticipantsList() }} + {% else %} + {{ EmptyState() }} + {% endif %} + +
+
+{% endblock %} + +{% macro ParticipantsList() %} +
+ {% for participant in participants %} +
+ {{ widgets.Avatar(member=participant, avatar_class="avatar-large") }} +
+ {{ participant.full_name }} +
+ {% for course in participant.courses %} +
+ {{ course }} +
+ {% endfor %} + +
+ {% endfor %} +
+{% endmacro %} + +{% macro EmptyState() %} +
+ +
+
{{ _("No Certified Participants") }}
+
{{ _("Enroll in a batch to get certified.") }}
+
+
+{% endmacro %} \ No newline at end of file diff --git a/lms/www/certified_participants/certified_participants.js b/lms/www/certified_participants/certified_participants.js new file mode 100644 index 00000000..af5b1340 --- /dev/null +++ b/lms/www/certified_participants/certified_participants.js @@ -0,0 +1,18 @@ +frappe.ready(() => { + $("#certificate-filter").change((e) => { + filter_certified_participants(); + }); +}); + +const filter_certified_participants = () => { + const certificate = $("#certificate-filter").val(); + $(".common-card-style").removeClass("hide"); + + if (certificate) { + $(".common-card-style").addClass("hide"); + $(`[data-course='${certificate}']`) + .closest(".common-card-style") + .removeClass("hide"); + console.log(certificate); + } +}; diff --git a/lms/www/certified_participants/certified_participants.py b/lms/www/certified_participants/certified_participants.py new file mode 100644 index 00000000..2dc9b238 --- /dev/null +++ b/lms/www/certified_participants/certified_participants.py @@ -0,0 +1,42 @@ +import frappe + + +def get_context(context): + context.no_cache = 1 + members = frappe.get_all( + "LMS Certificate", + filters={"published": 1}, + pluck="member", + order_by="issue_date desc", + distinct=1, + ) + + participants = [] + course_filter = [] + for member in members: + details = frappe.db.get_value( + "User", member, ["name", "full_name", "user_image", "username", "enabled"], as_dict=1 + ) + courses = frappe.get_all( + "LMS Certificate", + filters={"member": member, "published": 1}, + fields=["course", "issue_date"], + ) + details.courses = [] + for course in courses: + + if not details.issue_date: + details.issue_date = course.issue_date + + title = frappe.db.get_value("LMS Course", course.course, "title") + details.courses.append(title) + + if title not in course_filter: + course_filter.append(title) + + if details.enabled: + participants.append(details) + + participants = sorted(participants, key=lambda d: d.issue_date, reverse=True) + context.participants = participants + context.course_filter = course_filter diff --git a/lms/www/classes/index.html b/lms/www/classes/index.html deleted file mode 100644 index 081c8b09..00000000 --- a/lms/www/classes/index.html +++ /dev/null @@ -1,163 +0,0 @@ -{% extends "lms/templates/lms_base.html" %} -{% block title %} - {{ _("All Classes") }} -{% endblock %} - -{% block page_content %} -
-
- {{ Header() }} - {% if past_classes | length or upcoming_classes | length %} - {{ ClassTabs(past_classes, upcoming_classes, my_classes) }} - {% else %} - {{ EmptyState() }} - {% endif %} -
-
-{% endblock %} - -{% macro Header() %} -
-
{{ _("All Classes") }}
- {% if is_moderator %} - - {% endif %} -
-{% endmacro %} - -{% macro ClassTabs(past_classes, upcoming_classes, my_classes) %} -
- - -
- -
-
- {{ ClassCards(upcoming_classes) }} -
- - {% if is_moderator %} -
- {{ ClassCards(past_classes) }} -
- {% endif %} - - {% if frappe.session.user != "Guest" %} -
- {{ ClassCards(my_classes) }} -
- {% endif %} - -
-
-{% endmacro %} - -{% macro ClassCards(classes) %} -
- {% for class in classes %} - {% set course_count = frappe.db.count("Class Course", {"parent": class.name}) %} - {% set student_count = frappe.db.count("Class Student", {"parent": class.name}) %} - -
- -
- {{ class.title }} -
- - {% if class.description %} -
- {{ class.description }} -
- {% endif %} - -
- - - - - {{ frappe.utils.format_date(class.start_date, "medium") }} - - - - {{ frappe.utils.format_date(class.end_date, "medium") }} - -
- -
- - - - {{ course_count }} {{ _("Courses") }} -
- -
- - - - {{ student_count }} {{ _("Students") }} -
- - -
- {% endfor %} -
-{% endmacro %} - -{% macro EmptyState() %} -
- -
-
{{ _("No Classes") }}
-
{{ _("Nothing to see here.") }}
-
-
-{% endmacro %} - -{%- block script %} - {{ super() }} - {{ include_script('controls.bundle.js') }} - {% if is_moderator %} - - {% endif %} -{% endblock %} \ No newline at end of file diff --git a/lms/www/classes/index.py b/lms/www/classes/index.py deleted file mode 100644 index 4c0c83ff..00000000 --- a/lms/www/classes/index.py +++ /dev/null @@ -1,48 +0,0 @@ -import frappe -from frappe.utils import getdate -from lms.lms.utils import has_course_moderator_role - - -def get_context(context): - context.no_cache = 1 - context.is_moderator = has_course_moderator_role() - classes = frappe.get_all( - "LMS Class", - fields=[ - "name", - "title", - "description", - "start_date", - "end_date", - "paid_class", - "seat_count", - ], - ) - - past_classes, upcoming_classes = [], [] - for class_ in classes: - if getdate(class_.start_date) < getdate(): - past_classes.append(class_) - else: - upcoming_classes.append(class_) - - context.past_classes = sorted(past_classes, key=lambda d: d.start_date) - context.upcoming_classes = sorted(upcoming_classes, key=lambda d: d.start_date) - - if frappe.session.user != "Guest": - my_classes_info = [] - my_classes = frappe.get_all( - "Class Student", {"student": frappe.session.user}, pluck="parent" - ) - - for class_ in my_classes: - my_classes_info.append( - frappe.db.get_value( - "LMS Class", - class_, - ["name", "title", "start_date", "end_date", "paid_class", "seat_count"], - as_dict=True, - ) - ) - - context.my_classes = my_classes_info diff --git a/lms/www/classes/progress.js b/lms/www/classes/progress.js deleted file mode 100644 index 6c136c9a..00000000 --- a/lms/www/classes/progress.js +++ /dev/null @@ -1,5 +0,0 @@ -frappe.ready(() => { - $(".clickable-row").click((e) => { - window.location.href = $(e.currentTarget).data("href"); - }); -}); diff --git a/lms/www/courses/course.html b/lms/www/courses/course.html index cd294edd..b8f5b7fe 100644 --- a/lms/www/courses/course.html +++ b/lms/www/courses/course.html @@ -10,8 +10,8 @@ {{ CourseHomeHeader(course) }}
+ {{ CourseHeaderOverlay(course) }}
- {{ CourseHeaderOverlay(course) }} {{ Description(course) }} {{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }} {% if course.status == "Approved" and not frappe.utils.cint(course.upcoming) %} @@ -41,7 +41,7 @@ {% macro BreadCrumb(course) %} {% endmacro %} @@ -57,8 +57,8 @@ {% endfor %}
-
- {% if course.title %} {{ course.title }} {% endif %} +
+ {{ course.title }}
@@ -228,7 +228,7 @@ {% elif course.paid_course and not is_instructor %} - + {{ _("Buy This Course") }} diff --git a/lms/www/courses/index.html b/lms/www/courses/index.html index bb7ae213..52ea23f7 100644 --- a/lms/www/courses/index.html +++ b/lms/www/courses/index.html @@ -48,15 +48,15 @@ {% endif %} - {% if show_creators_section %} - - {{ _("Create a Course") }} - - {% endif %} - {{ _("Search") }} (Ctrl + k) + + {% if show_creators_section %} + + {{ _("Create a Course") }} + + {% endif %}
diff --git a/lms/www/courses/outline.html b/lms/www/courses/outline.html index 7d7805f3..2e662d81 100644 --- a/lms/www/courses/outline.html +++ b/lms/www/courses/outline.html @@ -146,7 +146,7 @@ {{ _("Short Description") }}
- {{ _("A breif description about this chapter.") }} + {{ _("A brief description about this chapter.") }}
diff --git a/lms/www/profiles/profile.html b/lms/www/profiles/profile.html index 640493db..40127207 100644 --- a/lms/www/profiles/profile.html +++ b/lms/www/profiles/profile.html @@ -65,8 +65,8 @@
{{ About(member) }} - {{ EducationDetails(member) }} {{ WorkDetails(member) }} + {{ EducationDetails(member) }} {{ ExternalCertification(member) }} {{ Contact(member) }} {{ Skills(member) }} @@ -171,7 +171,7 @@ {% macro CoursesMentored(member, read_only) %} {% if member.get_mentored_courses() | length %}
-
{{ _("Courses Mentored") }}
+
{{ _("Courses Mentored") }}
{% for course in member.get_mentored_courses() %} {{ widgets.CourseCard(course=course, read_only=read_only) }} @@ -202,7 +202,7 @@ {% if has_course_moderator_role() %}
-
{{ _("Role Settings") }}
+
{{ _("Role Settings") }}