diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33ad3b85..1eca3075 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,8 +46,8 @@ repos: lms/public/js/lib/.* )$ - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: ['flake8-bugbear',] diff --git a/lms/hooks.py b/lms/hooks.py index 8fb9cb6f..52a1a34f 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -95,7 +95,8 @@ override_doctype_class = { # Hook on document methods and events doc_events = { - "Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"} + "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 @@ -143,6 +144,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": "/courses//progress", "to_route": "batch/progress"}, {"from_route": "/courses//join", "to_route": "batch/join"}, {"from_route": "/courses//manage", "to_route": "cohorts"}, @@ -165,6 +167,11 @@ 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": "/assignments/", "to_route": "assignments/assignment"}, ] website_redirects = [ diff --git a/lms/lms/doctype/class_course/__init__.py b/lms/lms/doctype/class_course/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/class_course/class_course.json b/lms/lms/doctype/class_course/class_course.json new file mode 100644 index 00000000..f53619c4 --- /dev/null +++ b/lms/lms/doctype/class_course/class_course.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2022-11-09 16:23:26.454527", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "course", + "title" + ], + "fields": [ + { + "fieldname": "course", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Course", + "options": "LMS Course", + "reqd": 1 + }, + { + "fetch_from": "course.title", + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-11-11 15:51:45.560864", + "modified_by": "Administrator", + "module": "LMS", + "name": "Class Course", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/class_course/class_course.py b/lms/lms/doctype/class_course/class_course.py new file mode 100644 index 00000000..64b9f410 --- /dev/null +++ b/lms/lms/doctype/class_course/class_course.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ClassCourse(Document): + pass diff --git a/lms/lms/doctype/class_student/__init__.py b/lms/lms/doctype/class_student/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/class_student/class_student.js b/lms/lms/doctype/class_student/class_student.js new file mode 100644 index 00000000..69567d23 --- /dev/null +++ b/lms/lms/doctype/class_student/class_student.js @@ -0,0 +1,7 @@ +// Copyright (c) 2022, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Class Student", { + // refresh: function(frm) { + // } +}); diff --git a/lms/lms/doctype/class_student/class_student.json b/lms/lms/doctype/class_student/class_student.json new file mode 100644 index 00000000..3442dd7c --- /dev/null +++ b/lms/lms/doctype/class_student/class_student.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2022-11-09 16:20:44.602545", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "student", + "student_name", + "username" + ], + "fields": [ + { + "fieldname": "student", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student", + "options": "User", + "reqd": 1 + }, + { + "fetch_from": "student.full_name", + "fieldname": "student_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Student Name", + "read_only": 1 + }, + { + "fetch_from": "student.username", + "fieldname": "username", + "fieldtype": "Data", + "label": "Username", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-11-15 11:13:39.410578", + "modified_by": "Administrator", + "module": "LMS", + "name": "Class Student", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/class_student/class_student.py b/lms/lms/doctype/class_student/class_student.py new file mode 100644 index 00000000..cfe37fa1 --- /dev/null +++ b/lms/lms/doctype/class_student/class_student.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ClassStudent(Document): + pass diff --git a/lms/lms/doctype/class_student/test_class_student.py b/lms/lms/doctype/class_student/test_class_student.py new file mode 100644 index 00000000..41f8ed09 --- /dev/null +++ b/lms/lms/doctype/class_student/test_class_student.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestClassStudent(FrappeTestCase): + pass diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index 3a29ee4d..acc3bc7e 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -33,6 +33,7 @@ class CourseLesson(Document): e = frappe.get_doc(doctype_map[section], name) e.lesson = self.name e.index_ = index + e.course = self.course e.save(ignore_permissions=True) index += 1 self.update_orphan_documents(doctype_map[section], documents) @@ -49,6 +50,7 @@ class CourseLesson(Document): for name in orphan_documents: ex = frappe.get_doc(doctype, name) ex.lesson = None + ex.course = None ex.index_ = 0 ex.index_label = "" ex.save() diff --git a/lms/lms/doctype/lesson_assignment/lesson_assignment.json b/lms/lms/doctype/lesson_assignment/lesson_assignment.json index 18e41b8a..46f316f3 100644 --- a/lms/lms/doctype/lesson_assignment/lesson_assignment.json +++ b/lms/lms/doctype/lesson_assignment/lesson_assignment.json @@ -9,9 +9,11 @@ "assignment", "lesson", "course", + "status", "column_break_3", "member", - "member_name" + "member_name", + "comments" ], "fields": [ { @@ -59,12 +61,24 @@ "in_standard_filter": 1, "label": "Course", "read_only": 1 + }, + { + "default": "Not Graded", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Pass\nFail\nNot Graded" + }, + { + "fieldname": "comments", + "fieldtype": "Small Text", + "label": "Comments" } ], "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2022-10-31 13:18:09.609729", + "modified": "2022-11-16 12:11:59.472025", "modified_by": "Administrator", "module": "LMS", "name": "Lesson Assignment", @@ -85,6 +99,19 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [], + "states": [ + { + "color": "Green", + "title": "Pass" + }, + { + "color": "Orange", + "title": "Not Graded" + }, + { + "color": "Red", + "title": "Fail" + } + ], "title_field": "lesson" } \ No newline at end of file diff --git a/lms/lms/doctype/lesson_assignment/lesson_assignment.py b/lms/lms/doctype/lesson_assignment/lesson_assignment.py index 8c61faa2..9a22b52e 100644 --- a/lms/lms/doctype/lesson_assignment/lesson_assignment.py +++ b/lms/lms/doctype/lesson_assignment/lesson_assignment.py @@ -12,7 +12,8 @@ class LessonAssignment(Document): def validate_duplicates(self): if frappe.db.exists( - "Lesson Assignment", {"lesson": self.lesson, "member": self.member} + "Lesson Assignment", + {"lesson": self.lesson, "member": self.member, "name": ["!=", self.name]}, ): lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title") frappe.throw( @@ -43,10 +44,18 @@ def get_assignment(lesson): assignment = frappe.db.get_value( "Lesson Assignment", {"lesson": lesson, "member": frappe.session.user}, - ["lesson", "member", "assignment"], + ["lesson", "member", "assignment", "comments", "status"], as_dict=True, ) assignment.file_name = frappe.db.get_value( "File", {"file_url": assignment.assignment}, "file_name" ) return assignment + + +@frappe.whitelist() +def grade_assignment(name, result, comments): + doc = frappe.get_doc("Lesson Assignment", name) + doc.status = result + doc.comments = comments + doc.save() diff --git a/lms/lms/doctype/lms_class/__init__.py b/lms/lms/doctype/lms_class/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_class/lms_class.js b/lms/lms/doctype/lms_class/lms_class.js new file mode 100644 index 00000000..78508cbf --- /dev/null +++ b/lms/lms/doctype/lms_class/lms_class.js @@ -0,0 +1,7 @@ +// Copyright (c) 2022, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("LMS Class", { + // refresh: function(frm) { + // } +}); diff --git a/lms/lms/doctype/lms_class/lms_class.json b/lms/lms/doctype/lms_class/lms_class.json new file mode 100644 index 00000000..4cbaaeb9 --- /dev/null +++ b/lms/lms/doctype/lms_class/lms_class.json @@ -0,0 +1,103 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format: CLS-{#####}", + "creation": "2022-11-09 16:14:05.876933", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "start_date", + "end_date", + "column_break_4", + "description", + "section_break_6", + "students", + "courses" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "students", + "fieldtype": "Table", + "label": "Students", + "options": "Class Student" + }, + { + "fieldname": "courses", + "fieldtype": "Table", + "label": "Courses", + "options": "Class Course" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-11-21 10:55:50.067225", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Class", + "naming_rule": "Expression (old style)", + "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 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_class/lms_class.py new file mode 100644 index 00000000..281bbe78 --- /dev/null +++ b/lms/lms/doctype/lms_class/lms_class.py @@ -0,0 +1,66 @@ +# Copyright (c) 2022, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe import _ +from frappe.utils import cint + + +class LMSClass(Document): + def validate(self): + validate_membership(self) + + +def validate_membership(self): + for course in self.courses: + for student in self.students: + filters = { + "doctype": "LMS Batch Membership", + "member": student.student, + "course": course.course, + } + if not frappe.db.exists(filters): + frappe.get_doc(filters).save() + + +@frappe.whitelist() +def add_student(email, class_name): + if not frappe.db.exists("User", email): + frappe.throw(_("There is no such user. Please create a user with this Email ID.")) + + frappe.get_doc( + { + "doctype": "Class Student", + "student": email, + "student_name": frappe.db.get_value("User", email, "full_name"), + "parent": class_name, + "parenttype": "LMS Class", + "parentfield": "students", + } + ).save() + return True + + +@frappe.whitelist() +def remove_student(student, class_name): + frappe.db.delete("Class Student", {"student": student, "parent": class_name}) + return True + + +@frappe.whitelist() +def update_course(class_name, course, value): + if cint(value): + doc = frappe.get_doc( + { + "doctype": "Class Course", + "parent": class_name, + "course": course, + "parenttype": "LMS Class", + "parentfield": "courses", + } + ) + doc.save() + else: + frappe.db.delete("Class Course", {"parent": class_name, "course": course}) + return True diff --git a/lms/lms/doctype/lms_class/test_lms_class.py b/lms/lms/doctype/lms_class/test_lms_class.py new file mode 100644 index 00000000..6e1f2659 --- /dev/null +++ b/lms/lms/doctype/lms_class/test_lms_class.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSClass(FrappeTestCase): + pass diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json index 99ffc184..a3221040 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.json +++ b/lms/lms/doctype/lms_quiz/lms_quiz.json @@ -9,9 +9,12 @@ "field_order": [ "title", "questions", - "lesson", + "section_break_3", "max_attempts", - "time" + "time", + "column_break_5", + "lesson", + "course" ], "fields": [ { @@ -46,11 +49,27 @@ "fieldname": "time", "fieldtype": "Int", "label": "Time Per Question (in Seconds)" + }, + { + "fetch_from": "lesson.course", + "fieldname": "course", + "fieldtype": "Link", + "label": "Course", + "options": "LMS Course", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-19 17:54:41.685182", + "modified": "2022-11-15 15:36:39.585488", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz", @@ -69,8 +88,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_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 6879c773..6d5b4a47 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -60,6 +60,13 @@ class LMSQuiz(Document): return result[0] +def update_lesson_info(doc, method): + if doc.quiz_id: + frappe.db.set_value( + "LMS Quiz", doc.quiz_id, {"lesson": doc.name, "course": doc.course} + ) + + @frappe.whitelist() def quiz_summary(quiz, results): score = 0 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 a2b13222..80ca9ffd 100644 --- a/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json +++ b/lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json @@ -5,11 +5,12 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "member", - "member_name", - "column_break_3", "quiz", "score", + "course", + "column_break_3", + "member", + "member_name", "section_break_6", "result" ], @@ -46,7 +47,8 @@ "fetch_from": "member.full_name", "fieldname": "member_name", "fieldtype": "Data", - "label": "Member Name" + "label": "Member Name", + "read_only": 1 }, { "fieldname": "column_break_3", @@ -55,12 +57,20 @@ { "fieldname": "section_break_6", "fieldtype": "Section Break" + }, + { + "fetch_from": "quiz.course", + "fieldname": "course", + "fieldtype": "Link", + "label": "Course", + "options": "LMS Course", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-10-10 18:57:42.813738", + "modified": "2022-11-15 15:27:07.770945", "modified_by": "Administrator", "module": "LMS", "name": "LMS Quiz Submission", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index edb256b0..d17229ab 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -486,7 +486,7 @@ def redirect_to_courses_list(): def has_course_instructor_role(member=None): return frappe.db.get_value( "Has Role", - {"parent": member or frappe.session.user, "role": "Course Instructor"}, + {"parent": member or frappe.session.user, "role": "Instructor"}, "name", ) @@ -506,7 +506,7 @@ def can_create_courses(member=None): def has_course_moderator_role(member=None): return frappe.db.get_value( "Has Role", - {"parent": member or frappe.session.user, "role": "Course Moderator"}, + {"parent": member or frappe.session.user, "role": "Moderator"}, "name", ) diff --git a/lms/lms/web_form/class/__init__.py b/lms/lms/web_form/class/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/web_form/class/class.js b/lms/lms/web_form/class/class.js new file mode 100644 index 00000000..8f56ebb3 --- /dev/null +++ b/lms/lms/web_form/class/class.js @@ -0,0 +1,3 @@ +frappe.ready(function () { + // bind events here +}); diff --git a/lms/lms/web_form/class/class.json b/lms/lms/web_form/class/class.json new file mode 100644 index 00000000..f46c2faa --- /dev/null +++ b/lms/lms/web_form/class/class.json @@ -0,0 +1,87 @@ +{ + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 0, + "allow_incomplete": 0, + "allow_multiple": 0, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "apply_document_permissions": 0, + "button_label": "Save", + "creation": "2022-11-11 12:10:29.640675", + "custom_css": "", + "doc_type": "LMS Class", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "is_standard": 1, + "list_columns": [], + "login_required": 0, + "max_attachment_size": 0, + "modified": "2022-11-21 10:56:01.627821", + "modified_by": "Administrator", + "module": "LMS", + "name": "class", + "owner": "Administrator", + "payment_button_label": "Buy Now", + "published": 1, + "route": "class", + "show_attachments": 0, + "show_list": 0, + "show_sidebar": 0, + "success_title": "", + "success_url": "/classes", + "title": "Class", + "web_form_fields": [ + { + "allow_read_on_all_link_options": 0, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 0, + "label": "Title", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "start_date", + "fieldtype": "Date", + "hidden": 0, + "label": "Start Date", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "end_date", + "fieldtype": "Date", + "hidden": 0, + "label": "End Date", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "label": "Description", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + } + ] +} \ No newline at end of file diff --git a/lms/lms/web_form/class/class.py b/lms/lms/web_form/class/class.py new file mode 100644 index 00000000..80b7b873 --- /dev/null +++ b/lms/lms/web_form/class/class.py @@ -0,0 +1,6 @@ +import frappe + + +def get_context(context): + # do your magic here + pass diff --git a/lms/patches.txt b/lms/patches.txt index 012a7f4c..872f96c9 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -37,3 +37,4 @@ lms.patches.v0_0.set_courses_page_as_home lms.patches.v0_0.set_member_in_progress #09-11-2022 lms.patches.v0_0.convert_progress_to_float lms.patches.v0_0.add_pages_to_nav #11-11-2022 +lms.patches.v0_0.change_role_names diff --git a/lms/patches/v0_0/change_role_names.py b/lms/patches/v0_0/change_role_names.py new file mode 100644 index 00000000..3e01336f --- /dev/null +++ b/lms/patches/v0_0/change_role_names.py @@ -0,0 +1,6 @@ +import frappe + + +def execute(): + frappe.rename_doc("Role", "Course Instructor", "Instructor") + frappe.rename_doc("Role", "Course Moderator", "Moderator") diff --git a/lms/plugins.py b/lms/plugins.py index 0c9bdb63..f542143f 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -152,7 +152,8 @@ def assignment_renderer(detail): file_type = detail.split("-")[1] accept = supported_types[file_type] if file_type else "" return frappe.render_template( - "templates/assignment.html", {"question": question, "accept": accept} + "templates/assignment.html", + {"question": question, "accept": accept, "file_type": file_type}, ) diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 65ef4106..1a70cfc4 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -9,7 +9,21 @@ body { } input[type=checkbox] { - appearance: auto; + appearance: auto; + position: relative; + width: var(--checkbox-size)!important; + height: var(--checkbox-size); + margin-right: var(--checkbox-right-margin)!important; + background-repeat: no-repeat; + background-position: center; + border: 1px solid var(--gray-400); + box-sizing: border-box; + box-shadow: 0 1px 2px #0000001a; + border-radius: 4px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-print-color-adjust: exact; } .course-image { @@ -44,38 +58,38 @@ input[type=checkbox] { } .course-image .course-tags { - width: fit-content; + width: fit-content; } .course-card-pills { - background: #ffffff; - margin-left: 0; - margin-right: 0.5rem; - border-radius: var(--border-radius); - padding: 3.5px 8px; - font-size: 11px; - text-align: center; - letter-spacing: 0.011em; - text-transform: uppercase; - font-weight: 600; - color: var(--gray-900); - width: fit-content; - box-shadow: var(--shadow-sm); + background: #ffffff; + margin-left: 0; + margin-right: 0.5rem; + border-radius: var(--border-radius); + padding: 3.5px 8px; + font-size: 11px; + text-align: center; + letter-spacing: 0.011em; + text-transform: uppercase; + font-weight: 600; + color: var(--gray-900); + width: fit-content; + box-shadow: var(--shadow-sm); } .dark-pills { - background: rgba(25, 39, 52, 0.8); - color: #ffffff; + background: rgba(25, 39, 52, 0.8); + color: #ffffff; } .dark-pills img { - width: 0.75rem; - height: 0.75rem; + width: 0.75rem; + height: 0.75rem; } .common-page-style { - padding: 2rem 0 5rem; - padding-top: 3rem; - background-color: var(--bg-color); + padding: 2rem 0 5rem; + padding-top: 3rem; + background-color: var(--bg-color); } .common-card-style { @@ -83,7 +97,7 @@ input[type=checkbox] { background: #FFFFFF; border-radius: var(--border-radius-md); position: relative; - box-shadow: var(--shadow-sm); + box-shadow: var(--shadow-base); } .course-card { @@ -125,10 +139,10 @@ input[type=checkbox] { } .course-card-title { - font-weight: 600; - color: var(--gray-900); - margin-bottom: 1.25rem; - font-size: 1.125rem; + font-weight: 600; + color: var(--gray-900); + margin-bottom: 1.25rem; + font-size: 1.125rem; } .card-divider { @@ -148,7 +162,7 @@ input[type=checkbox] { @media (max-width: 400px) { .course-instructor { - margin-left: 0; + margin-left: 0; } } @@ -188,26 +202,26 @@ input[type=checkbox] { @media (max-width: 767px) { .cards-parent { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - -moz-column-gap: 16px; - column-gap: 16px; - row-gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + -moz-column-gap: 16px; + column-gap: 16px; + row-gap: 16px; } } @media (max-width: 375px) { .cards-parent { - grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); - -moz-column-gap: 24px; - column-gap: 24px; - row-gap: 24px; + grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); + -moz-column-gap: 24px; + column-gap: 24px; + row-gap: 24px; } } @media (min-width: 576px) and (max-width: 992px) { .container { - padding-left: 1rem; - padding-right: 1rem; + padding-left: 1rem; + padding-right: 1rem; } } @@ -277,15 +291,15 @@ input[type=checkbox] { @media (max-width: 1000px) { .course-card-wide { - width: 75%; - margin: 0 auto; + width: 75%; + margin: 0 auto; } } @media (max-width: 768px) { .course-card-wide { - width: 100%; - margin: 0; + width: 100%; + margin: 0; } } @@ -330,7 +344,7 @@ input[type=checkbox] { @media (max-width: 768px) { .wide-button { - padding: 0.5rem 4rem; + padding: 0.5rem 4rem; } } @@ -364,7 +378,7 @@ input[type=checkbox] { } .course-home-page .course-home-outline { - padding-bottom: 4rem; + padding-bottom: 4rem; } .course-home-page { @@ -373,16 +387,16 @@ input[type=checkbox] { } .chapter-title { - cursor: pointer; - border-radius: var(--border-radius-lg); - color: var(--gray-900); - display: flex; - align-items: center; + cursor: pointer; + border-radius: var(--border-radius-lg); + color: var(--gray-900); + display: flex; + align-items: center; } .chapter-description { - color: var(--gray-900); - font-size: var(--text-sm); + color: var(--gray-900); + font-size: var(--text-sm); } .course-content-parent .chapter-description { @@ -405,10 +419,10 @@ input[type=checkbox] { } .lesson-links { - display: flex; - align-items: center; - padding: 0.5rem; - color: var(--gray-900); + display: flex; + align-items: center; + padding: 0.5rem; + color: var(--gray-900); } .lesson-links:hover { @@ -419,7 +433,7 @@ input[type=checkbox] { } .lessons { - margin-left: 1.5rem; + margin-left: 1.5rem; } .member-card { @@ -429,14 +443,10 @@ input[type=checkbox] { padding: 2rem 1rem; } -.member-card .talk-title { - font-weight: bold; -} - .break { - flex-basis: 100%; - flex-grow: 1; - margin: 0.5rem 0; + flex-basis: 100%; + flex-grow: 1; + margin: 0.5rem 0; } .course-home-headings { @@ -477,8 +487,8 @@ input[type=checkbox] { @media (max-width: 500px) { .avatar-square { - width: 75px; - height: 75px; + width: 75px; + height: 75px; } } @@ -492,10 +502,10 @@ input[type=checkbox] { @media (max-width: 600px) { .member-parent { - grid-template-columns: repeat(auto-fill, minmax(125px, 1fr)); - -moz-column-gap: 2rem; - column-gap: 2rem; - row-gap: 2rem; + grid-template-columns: repeat(auto-fill, minmax(125px, 1fr)); + -moz-column-gap: 2rem; + column-gap: 2rem; + row-gap: 2rem; } } @@ -514,7 +524,7 @@ input[type=checkbox] { @media (max-width: 600px) { .review-modl .modal-dialog { - width: auto; + width: auto; } } @@ -563,12 +573,12 @@ input[type=checkbox] { } .lesson-content { - padding: 1.5rem; - flex-direction: column; + padding: 1.5rem; + flex-direction: column; } .lesson-content-card { - margin-top: 2rem; + margin-top: 2rem; } .lesson-content-card .alert-dismissible .close { @@ -583,21 +593,21 @@ input[type=checkbox] { @media (max-width: 1024px) { .course-content-parent { - display: flex; - flex-direction: column-reverse; + display: flex; + flex-direction: column-reverse; } } .course-content-parent .course-home-headings { - margin: 0 0 1rem; - width: 100%; + margin: 0 0 1rem; + width: 100%; } .lesson-pagination { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 2rem + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 2rem } .lesson-pagination-parent { @@ -606,7 +616,7 @@ input[type=checkbox] { @media (max-width: 768px) { .lesson-pagination-parent { - margin-left: 0px; + margin-left: 0px; } } @@ -637,7 +647,7 @@ input[type=checkbox] { @media (max-width: 500px) { .profile-banner { - height: 150px; + height: 150px; } } @@ -656,9 +666,9 @@ input[type=checkbox] { @media (max-width: 550px) { .profile-info { - align-items: flex-end; - padding-left: 0; - height: 150px; + align-items: flex-end; + padding-left: 0; + height: 150px; } } @@ -671,15 +681,15 @@ input[type=checkbox] { @media (max-width: 500px) { .profile-avatar { - top: 95px; - left: 10px; + top: 95px; + left: 10px; } } @media (max-width: 375px) { .profile-avatar { - top: 120px; - left: 10px; + top: 120px; + left: 10px; } } @@ -691,8 +701,8 @@ input[type=checkbox] { @media (max-width: 375px) { .profile-name { - font-size: var(--text-lg); - padding-top: 5px; + font-size: var(--text-lg); + padding-top: 5px; } } @@ -704,9 +714,9 @@ input[type=checkbox] { @media (max-width: 550px) { .profile-name-section { - flex-direction: column; - align-items: flex-end; - margin: 0 0 0.5rem; + flex-direction: column; + align-items: flex-end; + margin: 0 0 0.5rem; } } @@ -729,14 +739,14 @@ input[type=checkbox] { @media (max-width: 375px) { .creator-badge { - font-size: 8px; + font-size: 8px; } } @media (max-width: 550px) { .creator-badge { - margin-top: 0.25rem; - margin-left: 0; + margin-top: 0.25rem; + margin-left: 0; } } @@ -808,7 +818,7 @@ pre { } .markdown-source p:last-child { - margin-bottom: 0; + margin-bottom: 0; } .avatar img { @@ -816,34 +826,34 @@ pre { } .certificate-content { - background-color: #FFFFFF; - border-width: 10px; - border-style: solid; + background-color: #FFFFFF; + border-width: 10px; + border-style: solid; } @media (max-width: 500px) { - .certificate-content { - border-width: 50px; - } + .certificate-content { + border-width: 50px; + } } .certificate-footer { - display: flex; - justify-content: center; - margin: 4rem auto 0; - width: fit-content; + display: flex; + justify-content: center; + margin: 4rem auto 0; + width: fit-content; } .certificate-ribbon { - background-color: var(--primary-color); - padding: 0.5rem; - border-radius: var(--border-radius-md); + background-color: var(--primary-color); + padding: 0.5rem; + border-radius: var(--border-radius-md); } .certificate-heading { - font-size: 2rem; - font-weight: 500; - color: var(--text-color); + font-size: 2rem; + font-weight: 500; + color: var(--text-color); } .certificate-para { @@ -851,19 +861,19 @@ pre { } .certificate-card { - background: #FFFFFF; - border-radius: var(--border-radius-md); - position: relative; - box-shadow: var(--shadow-sm); - padding: 1rem; - text-align: center; + background: #FFFFFF; + border-radius: var(--border-radius-md); + position: relative; + box-shadow: var(--shadow-sm); + padding: 1rem; + text-align: center; } .certificate-footer-item { - color: var(--text-color); - font-weight: bold; - font-family: cursive; - font-size: 1.25rem; + color: var(--text-color); + font-weight: bold; + font-family: cursive; + font-size: 1.25rem; } .certificate-logo { @@ -871,20 +881,21 @@ pre { } @media (max-width: 768px) { - .certificate-card { - margin: 0; - } + .certificate-card { + margin: 0; + } } @media (max-width: 550px) { - .certificate-content { - padding: 1rem; - } + .certificate-content { + padding: 1rem; + } } .column-card { - flex-direction: column; - padding: 1.25rem; + flex-direction: column; + padding: 1.25rem; + height: 100%; } .empty-state { @@ -897,7 +908,7 @@ pre { } .empty-state-text { - margin-left: 1rem; + margin-left: 1rem; } .empty-state-heading { @@ -926,46 +937,42 @@ pre { } .course-search-header { - float: right; - width: 80%; - display: flex; - justify-content: flex-end; - align-items: center; + float: right; + width: 80%; + display: flex; + justify-content: flex-end; + align-items: center; } @media (max-width: 1250px) { - .search { - width: 40%; - } + .search { + width: 40%; + } } @media (max-width: 1000px) { - .search { - width: 55%; - } + .search { + width: 55%; + } - .course-search-header { - width: 75%; - } + .course-search-header { + width: 75%; + } } @media (max-width: 650px) { - .course-search-header { - width: 60%; - } + .course-search-header { + width: 60%; + } } @media (max-width: 550px) { - .course-search-header { - float: none; - width: 100%; - justify-content: space-between; - margin-bottom: 1rem; - } -} - -.section-heading { - font-size: var(--text-4xl); + .course-search-header { + float: none; + width: 100%; + justify-content: space-between; + margin-bottom: 1rem; + } } .testimonial-card { @@ -986,11 +993,11 @@ pre { } .testimonial-footer { - display: flex; - align-items: center; - border-top: 1px solid var(--gray-200); - padding-top: 2rem; - margin-top: auto; + display: flex; + align-items: center; + border-top: 1px solid var(--gray-200); + padding-top: 2rem; + margin-top: auto; } .testimonial-profession { @@ -1048,7 +1055,7 @@ pre { @media (max-width: 500px) { .carousel { - padding: 0 0.5rem; + padding: 0 0.5rem; } } @@ -1098,22 +1105,22 @@ pre { @media (min-width: 768px) { .lesson-pagination .custom-checkbox .empty-checkbox { - width: 1rem; - height: 1rem; - border-radius: var(--border-radius-sm); + width: 1rem; + height: 1rem; + border-radius: var(--border-radius-sm); } } @media (max-width: 767px) { .lesson-pagination .custom-checkbox .empty-checkbox { - margin-bottom: 1rem; - border-radius: var(--border-radius-sm); + margin-bottom: 1rem; + border-radius: var(--border-radius-sm); } .lesson-pagination .custom-checkbox span { - display: inline-block; - width: 70%; - font-size: 10px; + display: inline-block; + width: 70%; + font-size: 10px; } } @@ -1126,23 +1133,24 @@ pre { } .preview-work { - width: 50%; + display: flex; + align-items: center; } .job-card { - position: relative; - padding: 1rem; + position: relative; + padding: 1rem; } .company-logo { - background-position: center; - background-size: contain; - background-position: center; - background-repeat: no-repeat; - border-radius: var(--border-radius-sm); - width: 50px; - height: 50px; - margin-right: 1rem; + background-position: center; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + border-radius: var(--border-radius-sm); + width: 50px; + height: 50px; + margin-right: 1rem; } .job-card-parent { @@ -1151,9 +1159,9 @@ pre { } .job-card-logo-section { - display: flex; - align-items: center; - margin-top: 0.5rem; + display: flex; + align-items: center; + margin-top: 0.5rem; } .job-detail-card { @@ -1173,8 +1181,8 @@ pre { } .course-head-container { - color: var(--gray-900); - background-color: var(--gray-50); + color: var(--gray-900); + background-color: var(--gray-50); } .seperator { @@ -1196,12 +1204,12 @@ pre { @media (max-width: 1000px) { .course-overlay-card { - position: inherit; - margin: 1rem auto; + position: inherit; + margin: 1rem auto; } .seperator { - margin: 0 0.25rem; + margin: 0 0.25rem; } } @@ -1227,7 +1235,7 @@ pre { @media (max-width: 1000px) { .course-body-container { - width: 100%; + width: 100%; } } @@ -1270,25 +1278,25 @@ pre { @media (max-width: 1250px) { .reviews-header { - width: 85%; + width: 85%; } } @media (max-width: 768px) { .reviews-header { - width: 100%; + width: 100%; } } @media (max-width: 500px) { - .reviews-header { - flex-direction: column; - align-items: center; - } + .reviews-header { + flex-direction: column; + align-items: center; + } - .vertical-divider { - margin: 1rem; - } + .vertical-divider { + margin: 1rem; + } } .bold-heading { @@ -1307,8 +1315,8 @@ pre { } .vertical-divider { - border: 1px solid var(--gray-300); - margin: 0 1rem; + border: 1px solid var(--gray-300); + margin: 0 1rem; } .avg-rating-stars { @@ -1385,29 +1393,29 @@ pre { } .lms-nav .nav-link { - padding: var(--padding-sm) 0; - margin: 0 var(--margin-md); - font-size: var(--text-base); - color: var(--text-muted); + padding: var(--padding-sm) 0; + margin: 0 var(--margin-md); + font-size: var(--text-base); + color: var(--text-muted); } .lms-nav .nav-link.active { - font-weight: 500; - border-bottom: 1px solid var(--primary-color); - color: var(--primary-color); + font-weight: 500; + border-bottom: 1px solid var(--primary-color); + color: var(--primary-color); } @media (min-width: 500px) { - .lms-nav .nav-item:first-child .nav-link { - margin-left: 0; - } + .lms-nav .nav-item:first-child .nav-link { + margin-left: 0; + } } .dashboard-button { - position: relative; - top: -50px; - margin-left: auto; + position: relative; + top: -50px; + margin-left: auto; } .course-card-wide .breadcrumb { @@ -1419,300 +1427,300 @@ pre { } .btn-outline-primary { - border: 1px solid var(--primary-color); + border: 1px solid var(--primary-color); } .show-attachments { - padding-right: 0.5rem; - display: flex; - align-items: center; + padding-right: 0.5rem; + display: flex; + align-items: center; } .attachment-controls { - display: flex; - align-items: center; - width: fit-content; - cursor: pointer; + display: flex; + align-items: center; + width: fit-content; + cursor: pointer; } .attachments { - flex-direction: column; - padding: 0.5rem 0; - margin-top: 1rem; - position: absolute; - z-index: 1; - width: fit-content; - border-collapse: separate; - border-spacing: 1rem 0.5rem; + flex-direction: column; + padding: 0.5rem 0; + margin-top: 1rem; + position: absolute; + z-index: 1; + width: fit-content; + border-collapse: separate; + border-spacing: 1rem 0.5rem; } .attachments-parent { - color: var(--text-color); + color: var(--text-color); } li { - line-height: 1.7; + line-height: 1.7; } .course-overlay-title { - font-weight: 700; - font-size: var(--text-2xl); - line-height: 1rem; - color: var(--gray-900); - margin-bottom: 1.25rem; + font-weight: 700; + font-size: var(--text-2xl); + line-height: 1rem; + color: var(--gray-900); + margin-bottom: 1.25rem; } .course-card-wide .avatar .standard-image { - border: 1px solid var(--gray-400); + border: 1px solid var(--gray-400); } .lesson-progress-tick { - margin: 0 0.5rem + margin: 0 0.5rem } .no-preview { - color: var(--gray-600); + color: var(--gray-600); } .discussions-parent .empty-state { - background-color: var(--gray-200); + background-color: var(--gray-200); } .job-cards-parent { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); - -moz-column-gap: 40px; - grid-gap: 1rem; - align-items: center; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); + -moz-column-gap: 40px; + grid-gap: 1rem; + align-items: center; } .job-company { - display: flex; - align-items: center; + display: flex; + align-items: center; } .job-actions { - display: flex; - align-items: flex-start; - margin-left: auto; - margin-bottom: 1rem; + display: flex; + align-items: flex-start; + margin-left: auto; + margin-bottom: 1rem; } .job-detail-header { - display: flex; + display: flex; } @media (max-width: 600px) { - .job-company { - flex-direction: column; - align-items: inherit; - } + .job-company { + flex-direction: column; + align-items: inherit; + } } @media (max-width: 1200px) { - .job-cards-parent { - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); - } + .job-cards-parent { + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + } } @media (max-width: 500px) { - .job-cards-parent { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - } + .job-cards-parent { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + } - .job-detail-header { - flex-wrap: wrap; - } + .job-detail-header { + flex-wrap: wrap; + } - .job-actions { - margin-top: 1rem; - } + .job-actions { + margin-top: 1rem; + } } [contenteditable="true"] { - outline: none; - background-color: var(--bg-light-gray); - border-radius: var(--border-radius); - border: 1px dashed var(--gray-600); - padding: 0.5rem 0.75rem; - color: var(--gray-900); + outline: none; + background-color: var(--bg-light-gray); + border-radius: var(--border-radius); + border: 1px dashed var(--gray-600); + padding: 0.5rem 0.75rem; + color: var(--gray-900); } [contenteditable="true"]:empty:before { - content: attr(data-placeholder); - color: var(--gray-600); + content: attr(data-placeholder); + color: var(--gray-600); } .course-image-attachment { - margin-top: 0.25rem; - background-color: var(--bg-light-gray); - border-radius: var(--border-radius); - border: 1px dashed var(--gray-600); - padding: 0.5rem 0.75rem; - width: fit-content; + margin-top: 0.25rem; + background-color: var(--bg-light-gray); + border-radius: var(--border-radius); + border: 1px dashed var(--gray-600); + padding: 0.5rem 0.75rem; + width: fit-content; } .btn-delete-tag { - cursor: pointer; + cursor: pointer; } .chapter-edit { - border: 1px solid var(--dark-border-color); - border-radius: var(--border-radius); - padding: 1rem; - margin-bottom: 1rem; + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius); + padding: 1rem; + margin-bottom: 1rem; } .chapter-edit .lessons { - margin-left: 0; - margin-top: 2rem; + margin-left: 0; + margin-top: 2rem; } .chapter-parent { - margin-bottom: 1rem; + margin-bottom: 1rem; } .chapter-edit .chapter-title { - padding: 0.5rem 0; + padding: 0.5rem 0; } .course-card-pills[contenteditable="true"] { - box-shadow: none; + box-shadow: none; } .preview { - display: flex; - align-items: center; - font-size: var(--text-md); + display: flex; + align-items: center; + font-size: var(--text-md); } .table { - margin-bottom: 0; + margin-bottom: 0; } .quiz-card { - border: 1px solid var(--dark-border-color); - border-radius: var(--border-radius); - padding: 1.25rem; - margin-top: 1.25rem; - font-size: var(--text-base); + border: 1px solid var(--dark-border-color); + border-radius: var(--border-radius); + padding: 1.25rem; + margin-top: 1.25rem; + font-size: var(--text-base); } .option-input { - width: 45%; - margin-right: 1rem; + width: 45%; + margin-right: 1rem; } .option-checkbox { - width: 15%; - display: flex; - align-items: center; + width: 15%; + display: flex; + align-items: center; } .preview-video-header { - position: relative; + position: relative; } .preview-info { - position: absolute; - top: 0; - right: -30px; + position: absolute; + top: 0; + right: -30px; } .tool-tip { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .tool-tip .tooltiptext { - visibility: hidden; - width: 30rem; - background-color: var(--gray-800); - color: var(--fg-color); - padding: 1rem; - border-radius: var(--border-radius-md); - position: absolute; - z-index: 1; - opacity: 0; - transition: opacity 0.3s; + visibility: hidden; + width: 30rem; + background-color: var(--gray-800); + color: var(--fg-color); + padding: 1rem; + border-radius: var(--border-radius-md); + position: absolute; + z-index: 1; + opacity: 0; + transition: opacity 0.3s; } .tool-tip:hover .tooltiptext { - visibility: visible; - opacity: 1; + visibility: visible; + opacity: 1; } .tooltiptext ul { - padding: 1rem; + padding: 1rem; } .medium { - font-size: var(--text-base); + font-size: var(--text-base); } .quiz-row { - position: relative; - color: var(--text-color); - cursor: pointer; + position: relative; + color: var(--text-color); + cursor: pointer; } .course-creation-link { - float: right; + float: right; } @media (max-width: 500px) { - .course-creation-link { - float: inherit; - margin-bottom: 1rem; - } + .course-creation-link { + float: inherit; + margin-bottom: 1rem; + } } .indicator-pill::before { - width: 0; - height: 0; - margin-right: 0; + width: 0 !important; + height: 0 !important; + margin-right: 0 !important; } .role { - margin-bottom: 0; - cursor: pointer; + margin-bottom: 0; + cursor: pointer; } @media (min-width: 500px) { - .role:last-child { - margin-left: 5rem - } + .role:last-child { + margin-left: 5rem + } } .icon-xl { - width: 2.75rem; - height: 2.75rem; + width: 2.75rem; + height: 2.75rem; } .modal .comment-field { - height: 150px !important; - resize: auto !important; + height: 150px !important; + resize: auto !important; } .notification-card { - display: flex; - align-items: center; - margin-bottom: 1.5rem; - position: relative; + display: flex; + align-items: center; + margin-bottom: 1.5rem; + position: relative; } .notification-card:last-child { - margin-bottom: 0; + margin-bottom: 0; } .timestamp { - font-size: var(--text-xs); + font-size: var(--text-xs); } .stats-parent { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - grid-gap: 2rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 2rem; } .statistics .stats-parent { @@ -1721,90 +1729,150 @@ li { } .stats-label { - color: var(--gray-900); - font-weight: 500; + color: var(--gray-900); + font-weight: 500; } .stats-value { - color: var(--gray-900); - font-weight: 500; - font-size: 1.5rem; - margin-top: 2rem; + color: var(--gray-900); + font-weight: 500; + font-size: 1.5rem; + margin-top: 2rem; } .indicator-pill.green::before { - height: 0; - width: 0; - border-radius: 0; - margin-right: 0; + height: 0; + width: 0; + border-radius: 0; + margin-right: 0; } .modal-header { - padding: 1.5rem 1.5rem 0 !important; + padding: 1.5rem 1.5rem 0 !important; } .modal-body { - padding: 0 1.5rem !important; -} - -.modal-footer { - padding: 0.75rem 1.5rem !important; + padding: 0 1.5rem !important; } .modal-content { - font-size: var(--text-base) !important; + font-size: var(--text-base) !important; } .modal-header, .modal-body { - margin-bottom: 1rem !important; + margin-bottom: 1rem !important; } .modal-header { - border-bottom: none !important; + border-bottom: none !important; } .modal-footer { - border-top: none !important; - background-color: var(--gray-200) !important; - justify-content: flex-end !important; + padding: 0.75rem 1.5rem !important; + border-top: none !important; + background-color: var(--gray-200) !important; } .modal-header .modal-title { - color: var(--gray-900); + color: var(--gray-900); } .frappe-chart .title { - font-size: 1rem; - font-weight: 500; - fill: var(--gray-900); + font-size: 1rem; + font-weight: 500; + fill: var(--gray-900); } .course-description-section { - padding-bottom: 4rem; + padding-bottom: 4rem; } input::file-selector-button { - border-radius: var(--border-radius); - font-size: var(--text-md); - padding: 0.25rem 1.25rem; - border: none; - color: var(--text-color); - cursor: pointer; + border-radius: var(--border-radius); + font-size: var(--text-md); + padding: 0.25rem 1.25rem; + border: none; + color: var(--text-color); + cursor: pointer; } input::file-selector-button:hover { - background-color: var(--gray-400); + background-color: var(--gray-400); } .btn { - font-weight: 400; + font-weight: 400; } select { - appearance: none; - -webkit-appearance: none; + appearance: none; + -webkit-appearance: none; } .course-list-cta { float: right; } + +.modal-title { + font-size: var(--text-base) !important; +} + +.class-form-title { + font-size: var(--text-base); +} + +.remove-student { + cursor: pointer; +} + +.class-course-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 1rem; + font-size: var(--text-base); +} + +.class-cours { + cursor: pointer; +} + +.subheading { + font-weight: 500; + color: var(--gray-900); +} + +.progress-course-header { + display: flex; + background-color: var(--gray-100); + padding: 0.5rem; +} + +.section-heading { + font-size: 1rem; + color: var(--gray-900); + font-weight: 500; +} + +.table th { + color: var(--gray-900); + font-weight: 500; + border-bottom: 1px solid var(--gray-300); + border-top: none; +} + +.lms-dropdown { + border: 1px solid var(--gray-400); + border-radius: var(--border-radius-sm); + padding: 0.25rem 2rem; + cursor: pointer; + text-align: center; +} + +.lms-menu { + background-image: url(/assets/lms/icons/down-arrow.svg); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 0.75rem; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; +} diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index f60b4721..9b15f6a9 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -17,6 +17,14 @@ frappe.ready(() => { save_chapter(e); }); + $(".nav-link").click((e) => { + change_hash(e); + }); + + if (window.location.hash) { + open_tab(); + } + if (window.location.pathname == "/statistics") { generate_graph("New Signups", "#new-signups"); generate_graph("Course Enrollments", "#course-enrollments"); @@ -210,3 +218,11 @@ const generate_course_completion_graph = () => { }, }); }; + +const change_hash = (e) => { + window.location.hash = $(e.currentTarget).attr("href"); +}; + +const open_tab = () => { + $(`a[href="${window.location.hash}"]`).click(); +}; diff --git a/lms/templates/assignment.html b/lms/templates/assignment.html index f32fcb99..edee48ed 100644 --- a/lms/templates/assignment.html +++ b/lms/templates/assignment.html @@ -1,12 +1,22 @@
-
-

{{ _("Assignment") }}

-
{{ _(question) }}
- -
{{ _("Submit") }}
-
- -
{{ _("Change") }}
-
-
+
+

{{ _("Assignment") }}

+
{{ _(question) }}
+
+ {{ _("Only files of type {0} will be accepted.").format(file_type) }} +
+ +
{{ _("Submit") }}
+
+ +
+
{{ _("Change") }}
+
+
+ + {{ _("Instructors Comments") }}: + + +
+
diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html index babf9758..8629a2c9 100644 --- a/lms/templates/quiz.html +++ b/lms/templates/quiz.html @@ -1,114 +1,117 @@ {% if attempts_exceeded %}
-
{{ quiz.title }}
-
- {{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }} - {{ _("Your latest score is {0}.").format(last_attempt_score) }} -
+

+ {{ quiz.title }} +

+
+ {{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }} + {{ _("Your latest score is {0}.").format(last_attempt_score) }} +
{% else %} -
- {{ quiz.title }} -
-
- +
+ -
{{ quiz.title }}
+

+ {{ quiz.title }} +

-
- {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} -
+
+ {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} +
- {% if quiz.max_attempts %} - {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} -
- {{ _("This quiz can only be taken {0} {1}. If you attempt the quiz but leave the page before submitting, - the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }} -
- {% endif %} + {% if quiz.max_attempts %} + {% set suffix = "times" if quiz.max_attempts > 1 else "time" %} +
+ {{ _("This quiz can only be taken {0} {1}. If you attempt the quiz but leave the page before submitting, + the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }} +
+ {% endif %} - {% if quiz.time %} -
- {{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }} -
- {% endif %} + {% if quiz.time %} +
+ {{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }} +
+ {% endif %} -
+
-
-
- {% for question in quiz.questions %} - {% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %} + +
+ {% for question in quiz.questions %} + {% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %} -
-
-
{{ loop.index }}.
-
- {{ frappe.utils.md_to_html(question.question) }} -
-
{{ instruction }}
-
+
+
+
{{ loop.index }}.
+
+ {{ frappe.utils.md_to_html(question.question) }} +
+
{{ instruction }}
+
- {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} - {% for option in options %} - {% if option %} -
-
- -
+ {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} + {% for option in options %} + {% if option %} +
+
+ +
- {% set explanation = question['explanation_' + loop.index | string] %} - {% if explanation %} - {{ explanation }} - {% endif %} -
- {% endif %} - {% endfor %} + {% set explanation = question['explanation_' + loop.index | string] %} + {% if explanation %} + {{ explanation }} + {% endif %} +
+ {% endif %} + {% endfor %} -
- {% endfor %} -
+
+ {% endfor %} +
-
+
{% endif %} diff --git a/lms/www/assignments/__init__.py b/lms/www/assignments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/www/assignments/assignment.html b/lms/www/assignments/assignment.html new file mode 100644 index 00000000..f56cb38a --- /dev/null +++ b/lms/www/assignments/assignment.html @@ -0,0 +1,63 @@ +{% extends "templates/base.html" %} +{% block title %} + {{ _("Assignments") }} +{% endblock %} + + +{% block content %} +
+
+
+
{{ _("Assignment Grading") }}
+
+ + {{ _("Open Attachment") }} + + + +
+ + {{ _("Member") }}: + + + {{ assignment.member_name }} + +
+
+ {% set course_title = frappe.db.get_value("LMS Course", assignment.course, "title") %} + + {{ _("Course") }}: + + + {{ course_title }} + +
+
+ {% set lesson_title = frappe.db.get_value("Course Lesson", assignment.lesson, "title") %} + {{ _("Lesson") }}: + + {{ lesson_title }} + +
+ +
{{ _("Comments") }}:
+
+
+ +
+
+ +
+ {{ _("Save") }} +
+
+
+
+
+{% endblock %} diff --git a/lms/www/assignments/assignment.js b/lms/www/assignments/assignment.js new file mode 100644 index 00000000..aba82d71 --- /dev/null +++ b/lms/www/assignments/assignment.js @@ -0,0 +1,46 @@ +frappe.ready(() => { + this.result; + let self = this; + + set_result(); + + $("#save-assignment").click((e) => { + save_assignment(e); + }); + + $("#result").change((e) => { + $("#result option:selected").each(function () { + self.result = $(this).val(); + }); + }); +}); + +const set_result = () => { + let self = this; + let result = $("#result").data("type"); + if (result) { + $("#result option").each((i, elem) => { + if ($(elem).val() == result) { + $(elem).attr("selected", true); + self.result = result; + } + }); + } +}; + +const save_assignment = (e) => { + frappe.call({ + method: "lms.lms.doctype.lesson_assignment.lesson_assignment.grade_assignment", + args: { + name: $(e.currentTarget).data("assignment"), + result: self.result, + comments: $("#comments").val(), + }, + callback: (data) => { + frappe.show_alert({ + message: __("Saved"), + indicator: "green", + }); + }, + }); +}; diff --git a/lms/www/assignments/assignment.py b/lms/www/assignments/assignment.py new file mode 100644 index 00000000..dbbd5509 --- /dev/null +++ b/lms/www/assignments/assignment.py @@ -0,0 +1,22 @@ +import frappe +from lms.lms.utils import has_course_moderator_role +from frappe import _ + + +def get_context(context): + context.no_cache = 1 + assignment = frappe.form_dict["assignment"] + + if not has_course_moderator_role(): + message = "Only Moderators have access to this page." + if frappe.session.user == "Guest": + message = "Please login to access this page." + + raise frappe.PermissionError(_(message)) + + context.assignment = frappe.db.get_value( + "Lesson Assignment", + assignment, + ["assignment", "comments", "status", "name", "member_name", "course", "lesson"], + as_dict=True, + ) diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 52a8621f..dc79ee5b 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -412,10 +412,24 @@ const fetch_assignments = () => { callback: (data) => { if (data.message) { const assignment = data.message; + const status = assignment.status; let target = $(".attach-file"); target.addClass("hide"); target.siblings(".submit-work").addClass("hide"); target.siblings(".preview-work").removeClass("hide"); + if (status != "Not Graded") { + let color = status == "Pass" ? "green" : "red"; + $(".assignment-status") + .removeClass("hide") + .addClass(color) + .text(data.message.status); + target.siblings(".alert").addClass("hide"); + $(".clear-work").addClass("hide"); + if (assignment.comments) { + $(".comments").removeClass("hide"); + $(".comment").text(assignment.comments); + } + } target .siblings(".preview-work") .find("a") diff --git a/lms/www/classes/class.html b/lms/www/classes/class.html new file mode 100644 index 00000000..b763d742 --- /dev/null +++ b/lms/www/classes/class.html @@ -0,0 +1,146 @@ +{% extends "templates/base.html" %} +{% block title %} + {{ _(class_info.title) }} +{% endblock %} + + +{% block content %} +
+
+ {{ BreadCrumb(class_info) }} +
+ {{ ClassDetails(class_info) }} + {{ ClassSections(class_info, class_courses, class_students, published_courses) }} +
+
+
+{% endblock %} + + + +{% macro BreadCrumb(class_info) %} + +{% endmacro %} + + + +{% macro ClassDetails(class_info) %} +
+
+ {{ class_info.title }} +
+ {% if class_info.description %} +
+ {{ class_info.description }} +
+ {% endif %} +
+ {% if class_info.start_date %} + + {{ frappe.utils.format_date(class_info.start_date, "medium") }} - + + {% endif %} + {% if class_info.end_date %} + + {{ frappe.utils.format_date(class_info.end_date, "medium") }} + + {% endif %} +
+
+{% endmacro %} + + + +{% macro ClassSections(class_info, class_courses, class_students, published_courses) %} +
+ + +
+ +
+
+ {{ CoursesSection(class_info, class_courses, published_courses) }} +
+ +
+ {{ StudentsSection(class_info, class_students) }} +
+ +
+
+{% endmacro %} + + +{% macro CoursesSection(class_info, class_courses, published_courses) %} +
+ {% if published_courses | length %} + {% for course in published_courses %} + {% set checked = course.name in class_courses %} + + {% endfor %} + {% endif %} +
+{% endmacro %} + + +{% macro StudentsSection(class_info, class_students) %} +
+ {{ AddStudents() }} + + {% if class_students | length %} +
+ {% for student in class_students %} + + {% if not loop.last %}
{% endif %} + {% endfor %} +
+ {% else %} +

{{ _("No Students are added to this class.") }}

+ {% endif %} +
+{% endmacro %} + + +{% macro AddStudents() %} +
+
+ {{ _("Add Student") }} +
+
+
+
+ +
+
+ +
+
+{% endmacro %} diff --git a/lms/www/classes/class.js b/lms/www/classes/class.js new file mode 100644 index 00000000..23b9c3ca --- /dev/null +++ b/lms/www/classes/class.js @@ -0,0 +1,70 @@ +frappe.ready(() => { + $("#submit-student").click((e) => { + submit_student(e); + }); + + $(".remove-student").click((e) => { + remove_student(e); + }); + + $(".class-course").click((e) => { + update_course(e); + }); +}); + +const submit_student = (e) => { + e.preventDefault(); + frappe.call({ + method: "lms.lms.doctype.lms_class.lms_class.add_student", + args: { + email: $("#student-email").val(), + class_name: $(".class-details").data("class"), + }, + callback: (data) => { + frappe.show_alert( + { + message: __("Student added successfully"), + indicator: "green", + }, + 3 + ); + window.location.reload(); + }, + }); +}; + +const remove_student = (e) => { + frappe.confirm( + "Are you sure you want to remove this student from the class?", + () => { + frappe.call({ + method: "lms.lms.doctype.lms_class.lms_class.remove_student", + args: { + student: $(e.currentTarget).data("student"), + class_name: $(".class-details").data("class"), + }, + callback: (data) => { + frappe.show_alert( + { + message: __("Student removed successfully"), + indicator: "green", + }, + 3 + ); + window.location.reload(); + }, + }); + } + ); +}; + +const update_course = (e) => { + frappe.call({ + method: "lms.lms.doctype.lms_class.lms_class.update_course", + args: { + course: $(e.currentTarget).data("course"), + value: $(e.currentTarget).children("input").prop("checked") ? 1 : 0, + class_name: $(".class-details").data("class"), + }, + }); +}; diff --git a/lms/www/classes/class.py b/lms/www/classes/class.py new file mode 100644 index 00000000..14f598db --- /dev/null +++ b/lms/www/classes/class.py @@ -0,0 +1,34 @@ +import frappe +from lms.lms.utils import has_course_moderator_role +from frappe import _ + + +def get_context(context): + context.no_cache = 1 + + if not has_course_moderator_role(): + message = "Only Moderators have access to this page." + if frappe.session.user == "Guest": + message = "Please login to access this page." + + raise frappe.PermissionError(_(message)) + + class_name = frappe.form_dict["classname"] + + context.class_info = frappe.db.get_value( + "LMS Class", + class_name, + ["name", "title", "start_date", "end_date", "description"], + as_dict=True, + ) + context.published_courses = frappe.get_all( + "LMS Course", {"published": 1}, ["name", "title"] + ) + + context.class_courses = frappe.get_all( + "Class Course", {"parent": class_name}, pluck="course" + ) + + context.class_students = frappe.get_all( + "Class Student", {"parent": class_name}, ["student", "student_name", "username"] + ) diff --git a/lms/www/classes/index.html b/lms/www/classes/index.html new file mode 100644 index 00000000..065e1e1b --- /dev/null +++ b/lms/www/classes/index.html @@ -0,0 +1,66 @@ +{% extends "templates/base.html" %} +{% block title %} + {{ _("All Classes") }} +{% endblock %} + +{% block content %} +
+
+ + {{ _("Create Class") }} + +
{{ _("All Classes") }}
+ {% if classes %} + {{ ClassCards(classes) }} + {% else %} +
+ +
+
{{ _("No Classes") }}
+
{{ _("Nothing to see here.") }}
+
+
+ {% endif %} +
+
+{% endblock %} + + +{% 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}) %} + +
+
+ {% if course_count %} + + {{ course_count }} {{ _("Courses") }} + + {% endif %} + + {% if student_count %} + + {{ student_count }} {{ _("Students") }} + + {% endif %} +
+ +
+ {{ class.title }} +
+ +
+ + {{ frappe.utils.format_date(class.start_date, "medium") }} - + + + {{ frappe.utils.format_date(class.end_date, "medium") }} + +
+ +
+ {% endfor %} +
+{% endmacro %} diff --git a/lms/www/classes/index.py b/lms/www/classes/index.py new file mode 100644 index 00000000..7f5153f6 --- /dev/null +++ b/lms/www/classes/index.py @@ -0,0 +1,18 @@ +import frappe +from lms.lms.utils import has_course_moderator_role +from frappe import _ + + +def get_context(context): + context.no_cache = 1 + + if not has_course_moderator_role(): + message = "Only Moderators have access to this page." + if frappe.session.user == "Guest": + message = "Please login to access this page." + + raise frappe.PermissionError(_(message)) + + context.classes = frappe.get_all( + "LMS Class", fields=["name", "title", "start_date", "end_date"] + ) diff --git a/lms/www/classes/progress.html b/lms/www/classes/progress.html new file mode 100644 index 00000000..0eb1f5b4 --- /dev/null +++ b/lms/www/classes/progress.html @@ -0,0 +1,144 @@ +{% extends "templates/base.html" %} +{% block title %} + {{ student.first_name }} 's {{ _("Progress") }} +{% endblock %} + + +{% block content %} +
+
+ {{ BreadCrumb(class_info, student) }} +
+
+ {{ student.full_name }} +
+ {{ Progress(class_courses, student) }} +
+
+
+{% endblock %} + + +{% macro BreadCrumb(class_info, student) %} + +{% endmacro %} + + +{% macro Progress(class_info, student) %} +
+ {% for course in class_courses %} + {% set progress = course.membership.progress %} +
+
+
{{ course.title }}
+
{{ frappe.utils.cint(course.membership.progress) }}%
+
+ + {% if course.quizzes | length or course.assignments | length %} +
+ + + + + + + + + {% for quiz in course.quizzes %} + {% set filters = { "member": student.name, "course": course.course } %} + {% set has_submitted = frappe.db.exists("LMS Quiz Submission", filters) %} + {% set submission = frappe.db.get_value("LMS Quiz Submission", filters, ["score", "creation"], as_dict=True) %} + {% set total_questions = frappe.db.count("LMS Quiz Question", {"parent": quiz.name}) %} + + + + + {% if has_submitted %} + + + {% else %} + + + {% endif %} + + {% endfor %} + + {% for assignment in course.assignments %} + + {% set filters = { "member": student.name, "course": course.course, "lesson": assignment.name } %} + {% set has_submitted = frappe.db.exists("Lesson Assignment", filters) %} + {% set submission = frappe.db.get_value("Lesson Assignment", filters, ["assignment", "creation", "status"], as_dict=True) %} + {% set status = submission.status %} + {% set color = "green" if status == "Pass" else "red" if status == "Fail" else "orange" %} + + + + + {% if has_submitted %} + + + {% else %} + + + {% endif %} + + {% endfor %} +
+ {{ _("Activity") }} + + {{ _("Type") }} + + {{ _("Score/Status") }} + + {{ _("Last Attempt Date") }} +
+ {{ quiz.title }} + + {{ _("Quiz") }} + + {{ submission.score }}/{{ total_questions }} + + {{ frappe.utils.format_date(submission.creation, "medium") }} + + - + +
+ {{ _("Not Attempted") }} +
+
+ {{ assignment.title }} + + {{ _("Assignment") }} + + {% if status == "Not Graded" %} + {{ _("Grade") }} + {% else %} +
+ {{ status }} +
+ {% endif %} +
+ {{ frappe.utils.format_date(submission.creation, "medium") }} + + - + +
+ {{ _("Not Attempted") }} +
+
+
+ {% else %} +
+ {{ _("There are no activities in this course.") }} +
+ {% endif %} +
+ {% endfor %} +
+{% endmacro %} diff --git a/lms/www/classes/progress.py b/lms/www/classes/progress.py new file mode 100644 index 00000000..88b7397f --- /dev/null +++ b/lms/www/classes/progress.py @@ -0,0 +1,46 @@ +import frappe +from lms.lms.utils import has_course_moderator_role +from frappe import _ + + +def get_context(context): + context.no_cache = 1 + + if not has_course_moderator_role(): + message = "Only Moderators have access to this page." + if frappe.session.user == "Guest": + message = "Please login to access this page." + + raise frappe.PermissionError(_(message)) + + student = frappe.form_dict["username"] + classname = frappe.form_dict["classname"] + + context.student = frappe.db.get_value( + "User", {"username": student}, ["first_name", "full_name", "name"], as_dict=True + ) + context.class_info = frappe.db.get_value( + "LMS Class", classname, ["name"], as_dict=True + ) + + class_courses = frappe.get_all( + "Class Course", {"parent": classname}, ["course", "title"] + ) + + for course in class_courses: + course.membership = frappe.db.get_value( + "LMS Batch Membership", + {"member": context.student.name, "course": course.course}, + ["progress"], + as_dict=True, + ) + course.quizzes = frappe.get_all( + "LMS Quiz", {"course": course.course}, ["name", "title"] + ) + course.assignments = frappe.get_all( + "Course Lesson", + {"course": course.course, "question": ["is", "set"]}, + ["name", "title"], + ) + + context.class_courses = class_courses diff --git a/lms/www/courses/index.js b/lms/www/courses/index.js deleted file mode 100644 index f3bc44ec..00000000 --- a/lms/www/courses/index.js +++ /dev/null @@ -1,17 +0,0 @@ -frappe.ready(() => { - $(".nav-link").click((e) => { - change_hash(e); - }); - - if (window.location.hash) { - open_tab(); - } -}); - -const change_hash = (e) => { - window.location.hash = $(e.currentTarget).attr("href"); -}; - -const open_tab = () => { - $(`a[href="${window.location.hash}"]`).click(); -}; diff --git a/lms/www/profiles/profile.html b/lms/www/profiles/profile.html index 1a979f51..e355db95 100644 --- a/lms/www/profiles/profile.html +++ b/lms/www/profiles/profile.html @@ -205,14 +205,14 @@
{{ _("Role Settings") }}
diff --git a/lms/www/profiles/profile.js b/lms/www/profiles/profile.js index 9349c69b..7a5d90f3 100644 --- a/lms/www/profiles/profile.js +++ b/lms/www/profiles/profile.js @@ -4,14 +4,6 @@ frappe.ready(() => { $(".role").change((e) => { save_role(e); }); - - $(".nav-link").click((e) => { - change_hash(e); - }); - - if (window.location.hash) { - open_tab(); - } }); const make_profile_active_in_navbar = () => { @@ -46,11 +38,3 @@ const save_role = (e) => { }, }); }; - -const change_hash = (e) => { - window.location.hash = $(e.currentTarget).attr("href"); -}; - -const open_tab = () => { - $(`a[href="${window.location.hash}"]`).click(); -};