diff --git a/.editorconfig b/.editorconfig index dd608df2..0984f25b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -26,4 +26,4 @@ indent_style = tab # HTML, CSS, javascript, JSON and YAML [*.{html,css,js,json,yml,yaml}] -indent_size = 2 +indent_size = 4 diff --git a/school/hooks.py b/school/hooks.py index 53f67397..0a11804c 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -162,6 +162,8 @@ update_website_context = [ jinja = { "methods": [ "school.page_renderers.get_profile_url", + "school.overrides.user.get_enrolled_courses", + "school.overrides.user.get_course_membership", "school.overrides.user.get_authored_courses", "school.overrides.user.get_palette", "school.lms.utils.get_membership", @@ -186,7 +188,8 @@ jinja = { "school.lms.utils.get_sorted_reviews", "school.lms.utils.is_instructor", "school.lms.utils.convert_number_to_character", - "school.lms.utils.get_signup_optin_checks" + "school.lms.utils.get_signup_optin_checks", + "school.lms.utils.get_popular_courses" ], "filters": [] } diff --git a/school/lms/doctype/chapter_reference/chapter_reference.json b/school/lms/doctype/chapter_reference/chapter_reference.json index a251b1a4..0c67d748 100644 --- a/school/lms/doctype/chapter_reference/chapter_reference.json +++ b/school/lms/doctype/chapter_reference/chapter_reference.json @@ -1,5 +1,6 @@ { "actions": [], + "autoname": "hash", "creation": "2021-07-27 16:25:02.903245", "doctype": "DocType", "editable_grid": 1, @@ -20,13 +21,15 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-30 10:35:30.014950", + "modified": "2022-03-15 09:39:41.937565", "modified_by": "Administrator", "module": "LMS", "name": "Chapter Reference", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/school/lms/doctype/course_chapter/course_chapter.json b/school/lms/doctype/course_chapter/course_chapter.json index 7dca741a..ee8544bf 100644 --- a/school/lms/doctype/course_chapter/course_chapter.json +++ b/school/lms/doctype/course_chapter/course_chapter.json @@ -59,7 +59,7 @@ "link_fieldname": "chapter" } ], - "modified": "2021-09-29 15:33:44.611229", + "modified": "2022-03-14 17:57:00.707416", "modified_by": "Administrator", "module": "LMS", "name": "Course Chapter", @@ -77,11 +77,26 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, + "share": 1, + "write": 1 } ], "search_fields": "title", + "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/school/lms/doctype/course_lesson/course_lesson.json b/school/lms/doctype/course_lesson/course_lesson.json index d63eb69b..c38059ac 100644 --- a/school/lms/doctype/course_lesson/course_lesson.json +++ b/school/lms/doctype/course_lesson/course_lesson.json @@ -1,6 +1,6 @@ { "actions": [], - "allow_import": 1, + "allow_events_in_timeline": 1, "allow_rename": 1, "autoname": "format:{####} {title}", "creation": "2021-05-03 06:21:12.995984", @@ -9,6 +9,7 @@ "engine": "InnoDB", "field_order": [ "chapter", + "course", "include_in_preview", "column_break_4", "title", @@ -69,11 +70,20 @@ { "fieldname": "help", "fieldtype": "HTML" + }, + { + "fetch_from": "chapter.course", + "fieldname": "course", + "fieldtype": "Link", + "hidden": 1, + "label": "Course", + "options": "LMS Course", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-10-11 15:07:38.134808", + "modified": "2022-03-14 18:56:31.969801", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", @@ -89,11 +99,26 @@ "read": 1, "report": 1, "role": "System Manager", + "select": 1, + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, "share": 1, "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/school/lms/doctype/course_lesson/course_lesson.py b/school/lms/doctype/course_lesson/course_lesson.py index 38adb6a3..ee0023b6 100644 --- a/school/lms/doctype/course_lesson/course_lesson.py +++ b/school/lms/doctype/course_lesson/course_lesson.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from ...md import find_macros -from school.lms.utils import get_course_progress +from school.lms.utils import get_course_progress, get_lesson_url class CourseLesson(Document): def validate(self): @@ -108,3 +108,7 @@ def save_progress(lesson, course, status): progress = get_course_progress(course) frappe.db.set_value("LMS Batch Membership", membership, "progress", progress) return progress + +@frappe.whitelist() +def get_lesson_info(chapter): + return frappe.db.get_value("Course Chapter", chapter, "course") diff --git a/school/lms/doctype/lesson_reference/lesson_reference.json b/school/lms/doctype/lesson_reference/lesson_reference.json index a3e77ebb..86129694 100644 --- a/school/lms/doctype/lesson_reference/lesson_reference.json +++ b/school/lms/doctype/lesson_reference/lesson_reference.json @@ -1,5 +1,6 @@ { "actions": [], + "autoname": "hash", "creation": "2021-07-27 16:25:48.269536", "doctype": "DocType", "editable_grid": 1, @@ -20,13 +21,15 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-30 10:35:47.832547", + "modified": "2022-03-15 09:39:29.495991", "modified_by": "Administrator", "module": "LMS", "name": "Lesson Reference", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/school/lms/doctype/lms_course/lms_course.json b/school/lms/doctype/lms_course/lms_course.json index 84abccb2..671e6efb 100644 --- a/school/lms/doctype/lms_course/lms_course.json +++ b/school/lms/doctype/lms_course/lms_course.json @@ -7,10 +7,9 @@ "label": "Reindex Exercises" } ], - "allow_guest_to_view": 1, "allow_import": 1, "allow_rename": 1, - "creation": "2022-02-08 16:34:42.721203", + "creation": "2022-02-22 15:28:26.091549", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -21,13 +20,14 @@ "column_break_3", "instructors", "tags", + "status", "section_break_7", "is_published", - "column_break_9", + "column_break_10", "upcoming", - "column_break_11", + "column_break_12", "disable_self_learning", - "section_break_5", + "section_break_18", "short_introduction", "description", "chapters", @@ -43,7 +43,8 @@ "in_list_view": 1, "label": "Title", "reqd": 1, - "unique": 1 + "unique": 1, + "width": "200" }, { "fieldname": "description", @@ -66,10 +67,6 @@ "fieldtype": "Data", "label": "Video Embed Link" }, - { - "fieldname": "section_break_5", - "fieldtype": "Section Break" - }, { "fieldname": "short_introduction", "fieldtype": "Small Text", @@ -109,6 +106,7 @@ "fieldtype": "Table MultiSelect", "in_standard_filter": 1, "label": "Instructors", + "max_height": "50px", "options": "Course Instructor" }, { @@ -116,14 +114,6 @@ "fieldtype": "Section Break", "label": "Course Settings" }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, { "fieldname": "certification_section", "fieldtype": "Section Break", @@ -148,9 +138,31 @@ "fieldtype": "Table", "label": "Related Courses", "options": "Related Courses" + }, + { + "default": "In Progress", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "In Progress\nUnder Review\nApproved", + "read_only": 1 + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } ], - "index_web_pages_for_search": 1, "is_published_field": "is_published", "links": [ { @@ -174,7 +186,7 @@ "link_fieldname": "course" } ], - "modified": "2022-02-16 11:50:20.661085", + "modified": "2022-03-15 10:16:53.796878", "modified_by": "Administrator", "module": "LMS", "name": "LMS Course", @@ -189,6 +201,20 @@ "read": 1, "report": 1, "role": "System Manager", + "select": 1, + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, "share": 1, "write": 1 } diff --git a/school/lms/doctype/lms_course/lms_course.py b/school/lms/doctype/lms_course/lms_course.py index 24b2c9c2..94648505 100644 --- a/school/lms/doctype/lms_course/lms_course.py +++ b/school/lms/doctype/lms_course/lms_course.py @@ -12,6 +12,24 @@ from school.lms.utils import get_chapters class LMSCourse(Document): + def validate(self): + self.validate_instructors() + self.validate_status() + + def validate_instructors(self): + if self.is_new() and not self.instructors: + frappe.get_doc({ + "doctype": "Course Instructor", + "instructor": self.owner, + "parent": self.name, + "parentfield": "instructors", + "parenttype": "LMS Course" + }).save(ignore_permissions=True) + + def validate_status(self): + if self.is_published: + self.status = "Approved" + def on_update(self): if not self.upcoming and self.has_value_changed("upcoming"): self.send_email_to_interested_users() @@ -184,3 +202,11 @@ def search_course(text): }) """ return courses + +@frappe.whitelist() +def submit_for_review(course): + chapters = frappe.get_all("Chapter Reference", {"parent": course}) + if not len(chapters): + return "No Chp" + frappe.db.set_value("LMS Course", course, "status", "Under Review") + return "OK" diff --git a/school/lms/doctype/lms_course/test_lms_course.py b/school/lms/doctype/lms_course/test_lms_course.py index 64d27695..1dcefab0 100644 --- a/school/lms/doctype/lms_course/test_lms_course.py +++ b/school/lms/doctype/lms_course/test_lms_course.py @@ -12,8 +12,6 @@ class TestLMSCourse(unittest.TestCase): frappe.db.sql('delete from `tabLMS Course Mentor Mapping`') frappe.db.sql('delete from `tabLMS Course`') - - def test_new_course(self): course = new_course("Test Course") assert course.title == "Test Course" diff --git a/school/lms/doctype/lms_settings/lms_settings.json b/school/lms/doctype/lms_settings/lms_settings.json index 53197ad4..2fa1f3f5 100644 --- a/school/lms/doctype/lms_settings/lms_settings.json +++ b/school/lms/doctype/lms_settings/lms_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "show_search", + "portal_course_creation", "search_placeholder", "column_break_2", "force_profile_completion", @@ -107,8 +108,14 @@ "fieldtype": "Column Break" }, { - "fieldname": "column_break_9", - "fieldtype": "Column Break" + "default": "0", + "fieldname": "portal_course_creation", + "fieldtype": "Check", + "label": "Enable Course Creation from Portal" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" }, { "default": "0", @@ -128,7 +135,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-03-09 12:40:27.464136", + "modified": "2022-03-10 18:45:58.372370", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", @@ -149,4 +156,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/school/lms/doctype/lms_settings/lms_settings.py b/school/lms/doctype/lms_settings/lms_settings.py index 0681a991..8d9ef1ed 100644 --- a/school/lms/doctype/lms_settings/lms_settings.py +++ b/school/lms/doctype/lms_settings/lms_settings.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe import _ class LMSSettings(Document): - pass + pass @frappe.whitelist() def check_profile_restriction(): diff --git a/school/lms/utils.py b/school/lms/utils.py index 7c77288f..4df6f045 100644 --- a/school/lms/utils.py +++ b/school/lms/utils.py @@ -331,3 +331,16 @@ def get_signup_optin_checks(): links.append("" + mapper[check].get("title") + "") return (", ").join(links) + +def get_popular_courses(): + courses = frappe.get_all("LMS Course", {"is_published": 1, "upcoming": 0}) + course_membership = [] + + for course in courses: + course_membership.append({ + "course": course.name, + "members": cint(frappe.db.count("LMS Batch Membership", {"course": course.name})) + }) + + course_membership = sorted(course_membership, key = lambda x: x.get("members"), reverse=True) + return course_membership[:3] diff --git a/school/lms/web_form/chapter/__init__.py b/school/lms/web_form/chapter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/web_form/chapter/chapter.js b/school/lms/web_form/chapter/chapter.js new file mode 100644 index 00000000..6d710013 --- /dev/null +++ b/school/lms/web_form/chapter/chapter.js @@ -0,0 +1,8 @@ +frappe.ready(function() { + frappe.web_form.after_save = () => { + frappe.msgprint(__("Chapter has been saved successfully. Go back to the course and add this chapter to the chapters table.")) + setTimeout(() => { + window.location.href = `/courses/${frappe.web_form.doc.course}`; + }, 3000); + } +}); diff --git a/school/lms/web_form/chapter/chapter.json b/school/lms/web_form/chapter/chapter.json new file mode 100644 index 00000000..fc2b8afc --- /dev/null +++ b/school/lms/web_form/chapter/chapter.json @@ -0,0 +1,89 @@ +{ + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "apply_document_permissions": 1, + "button_label": "Save", + "creation": "2022-03-07 18:41:07.058806", + "doc_type": "Course Chapter", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "is_multi_step_form": 0, + "is_standard": 1, + "login_required": 1, + "max_attachment_size": 0, + "modified": "2022-03-14 18:48:01.704325", + "modified_by": "Administrator", + "module": "LMS", + "name": "chapter", + "owner": "Administrator", + "payment_button_label": "Buy Now", + "published": 1, + "route": "chapter", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 0, + "sidebar_items": [], + "success_url": "/chapter", + "title": "Chapter", + "web_form_fields": [ + { + "allow_read_on_all_link_options": 0, + "fieldname": "course", + "fieldtype": "Link", + "hidden": 0, + "label": "Course", + "max_length": 0, + "max_value": 0, + "options": "LMS Course", + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "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": "description", + "fieldtype": "Small Text", + "hidden": 0, + "label": "Description", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "lessons", + "fieldtype": "Table", + "hidden": 0, + "label": "Lessons", + "max_length": 0, + "max_value": 0, + "options": "Lesson Reference", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + } + ] +} \ No newline at end of file diff --git a/school/lms/web_form/chapter/chapter.py b/school/lms/web_form/chapter/chapter.py new file mode 100644 index 00000000..e1ada619 --- /dev/null +++ b/school/lms/web_form/chapter/chapter.py @@ -0,0 +1,5 @@ +import frappe + +def get_context(context): + # do your magic here + pass diff --git a/school/lms/web_form/course/__init__.py b/school/lms/web_form/course/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/web_form/course/course.js b/school/lms/web_form/course/course.js new file mode 100644 index 00000000..ff539e0a --- /dev/null +++ b/school/lms/web_form/course/course.js @@ -0,0 +1,6 @@ +frappe.ready(function() { + frappe.web_form.after_save = () => { + let route = frappe.web_form.doc.name ? `/courses/${frappe.web_form.doc.name}` : `/course`; + window.location.href = route; + } +}); diff --git a/school/lms/web_form/course/course.json b/school/lms/web_form/course/course.json new file mode 100644 index 00000000..f12d692e --- /dev/null +++ b/school/lms/web_form/course/course.json @@ -0,0 +1,147 @@ +{ + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "apply_document_permissions": 1, + "button_label": "Save", + "creation": "2022-03-07 14:40:41.262163", + "custom_css": "", + "doc_type": "LMS Course", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "is_multi_step_form": 0, + "is_standard": 1, + "login_required": 1, + "max_attachment_size": 0, + "modified": "2022-03-11 12:32:22.512115", + "modified_by": "Administrator", + "module": "LMS", + "name": "course", + "owner": "Administrator", + "payment_button_label": "Buy Now", + "published": 1, + "route": "course", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 0, + "sidebar_items": [], + "success_url": "/course", + "title": "Course", + "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": "tags", + "fieldtype": "Data", + "hidden": 0, + "label": "Tags", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "video_link", + "fieldtype": "Data", + "hidden": 0, + "label": "Video Embed Link", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 0, + "label": "Preview Image", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Section Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "short_introduction", + "fieldtype": "Small Text", + "hidden": 0, + "label": "Short Introduction", + "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": "Text", + "hidden": 0, + "label": "Description", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "chapters", + "fieldtype": "Table", + "hidden": 0, + "label": "Chapters", + "max_length": 0, + "max_value": 0, + "options": "Chapter Reference", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + } + ] +} \ No newline at end of file diff --git a/school/lms/web_form/course/course.py b/school/lms/web_form/course/course.py new file mode 100644 index 00000000..e1ada619 --- /dev/null +++ b/school/lms/web_form/course/course.py @@ -0,0 +1,5 @@ +import frappe + +def get_context(context): + # do your magic here + pass diff --git a/school/lms/web_form/lesson/__init__.py b/school/lms/web_form/lesson/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/web_form/lesson/lesson.js b/school/lms/web_form/lesson/lesson.js new file mode 100644 index 00000000..0044ba2d --- /dev/null +++ b/school/lms/web_form/lesson/lesson.js @@ -0,0 +1,16 @@ +frappe.ready(function() { + frappe.web_form.after_save = () => { + frappe.call({ + method: "school.lms.doctype.course_lesson.course_lesson.get_lesson_info", + args: { + "chapter": frappe.web_form.doc.chapter + }, + callback: (data) => { + frappe.msgprint(__(`Lesson has been saved successfully. Go back to the chapter and add this lesson to the lessons table.`)); + setTimeout(() => { + window.location.href = `/courses/${data.message}`; + }, 3000); + } + }); + }; +}); diff --git a/school/lms/web_form/lesson/lesson.json b/school/lms/web_form/lesson/lesson.json new file mode 100644 index 00000000..2c519587 --- /dev/null +++ b/school/lms/web_form/lesson/lesson.json @@ -0,0 +1,91 @@ +{ + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "apply_document_permissions": 1, + "button_label": "Save", + "creation": "2022-03-07 18:41:42.549831", + "custom_css": "", + "doc_type": "Course Lesson", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "introduction_text": "



Create lessons for your course. You can add some additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.


Content TypeSyntax
Video{{ Video(\"url_of_source\") }}
YouTube Video{{ YouTubeVideo(\"unique_embed_id\") }}
Exercise{{ Exercise(\"exercise_name\") }}
Quiz{{ Quiz(\"lms_quiz_name\") }}
Assignment{{ Assignment(\"id-filetype\") }}
", + "is_multi_step_form": 0, + "is_standard": 1, + "login_required": 1, + "max_attachment_size": 0, + "modified": "2022-03-14 18:49:33.526455", + "modified_by": "Administrator", + "module": "LMS", + "name": "lesson", + "owner": "Administrator", + "payment_button_label": "Buy Now", + "published": 1, + "route": "lesson", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 0, + "sidebar_items": [], + "success_url": "/lesson", + "title": "Lesson", + "web_form_fields": [ + { + "allow_read_on_all_link_options": 0, + "fieldname": "chapter", + "fieldtype": "Link", + "hidden": 0, + "label": "Course Chapter", + "max_length": 0, + "max_value": 0, + "options": "Course Chapter", + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "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, + "default": "0", + "fieldname": "include_in_preview", + "fieldtype": "Check", + "hidden": 0, + "label": "Include In Preview", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "body", + "fieldtype": "Text", + "hidden": 0, + "label": "Body", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + } + ] +} \ No newline at end of file diff --git a/school/lms/web_form/lesson/lesson.py b/school/lms/web_form/lesson/lesson.py new file mode 100644 index 00000000..e1ada619 --- /dev/null +++ b/school/lms/web_form/lesson/lesson.py @@ -0,0 +1,5 @@ +import frappe + +def get_context(context): + # do your magic here + pass diff --git a/school/lms/web_template/courses_enrolled/courses_enrolled.html b/school/lms/web_template/courses_enrolled/courses_enrolled.html index b45c9e12..f254a3ef 100644 --- a/school/lms/web_template/courses_enrolled/courses_enrolled.html +++ b/school/lms/web_template/courses_enrolled/courses_enrolled.html @@ -1,35 +1,27 @@ -{% set member = frappe.get_doc("User", frappe.session.user) %} -
- {% set enrolled = member.get_enrolled_courses().in_progress + member.get_enrolled_courses().completed %} - {% if enrolled | length %} -
-
- {{ _("Courses Enrolled") }} -
-
- {% for course in enrolled %} - {{ widgets.CourseCard(course=course) }} - {% endfor %} -
-
- {% else %} - {% set site_name = frappe.db.get_single_value("System Settings", "app_name") %} -
-
-
-
{{ _("You haven't enrolled for any courses") }}
-
{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}
-
- {% set recommended_courses = [course1, course2, course3] %} -
- {% for recommended_course in recommended_courses %} - {% if recommended_course %} - {% set course_details = frappe.get_doc("LMS Course", recommended_course) %} - {{ widgets.CourseCard(course=course_details) }} - {% endif %} - {% endfor %} -
-
-
- {% endif %} +{% set enrolled = get_enrolled_courses().in_progress + get_enrolled_courses().completed %} +{% if enrolled | length %} +
+ {% for course in enrolled %} + {{ widgets.CourseCard(course=course) }} + {% endfor %}
+{% else %} +{% set site_name = frappe.db.get_single_value("System Settings", "app_name") %} +
+
+
+
{{ _("You haven't enrolled for any courses") }}
+
{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}
+
+ {% set recommended_courses = get_popular_courses() %} +
+ {% for course in recommended_courses %} + {% if course %} + {% set course_details = frappe.get_doc("LMS Course", course.course) %} + {{ widgets.CourseCard(course=course_details) }} + {% endif %} + {% endfor %} +
+
+
+{% endif %} diff --git a/school/lms/web_template/courses_enrolled/courses_enrolled.json b/school/lms/web_template/courses_enrolled/courses_enrolled.json index 9571085f..cf482cac 100644 --- a/school/lms/web_template/courses_enrolled/courses_enrolled.json +++ b/school/lms/web_template/courses_enrolled/courses_enrolled.json @@ -3,41 +3,9 @@ "creation": "2021-10-21 11:29:50.424865", "docstatus": 0, "doctype": "Web Template", - "fields": [ - { - "__unsaved": 1, - "fieldname": "recommended_courses", - "fieldtype": "Section Break", - "label": "Recommended Courses for new users", - "reqd": 0 - }, - { - "__unsaved": 1, - "fieldname": "course1", - "fieldtype": "Link", - "label": "Course 1", - "options": "LMS Course", - "reqd": 1 - }, - { - "__unsaved": 1, - "fieldname": "course2", - "fieldtype": "Link", - "label": "Course 2", - "options": "LMS Course", - "reqd": 1 - }, - { - "__unsaved": 1, - "fieldname": "course3", - "fieldtype": "Link", - "label": "Course 3", - "options": "LMS Course", - "reqd": 1 - } - ], + "fields": [], "idx": 0, - "modified": "2021-10-28 19:36:11.372396", + "modified": "2022-03-14 09:44:28.266320", "modified_by": "Administrator", "module": "LMS", "name": "Courses Enrolled", diff --git a/school/lms/widgets/CourseCard.html b/school/lms/widgets/CourseCard.html index c9255b47..28d7ee8f 100644 --- a/school/lms/widgets/CourseCard.html +++ b/school/lms/widgets/CourseCard.html @@ -25,8 +25,15 @@
{% endif %} + {% if course.status and course.status != "Approved"%} + {% set pill_color = "gray" if course.status == "In Progress" else "orange" %} +
{{ course.status }}
+ {% endif %} + + {% set student_count = get_students(course.name) | length %} + {% set avg_rating = get_average_rating(course.name) %} + {% if student_count and avg_rating %}
- {% set student_count = get_students(course.name) | length %} {% if student_count %}
@@ -36,7 +43,6 @@
{% endif %} - {% set avg_rating = get_average_rating(course.name) %} {% if avg_rating %}
@@ -46,6 +52,7 @@
{% endif %}
+ {% endif %}
{{ course.title }}
@@ -59,7 +66,7 @@
{{ progress }}% Completed
{% endif %} - diff --git a/school/overrides/user.py b/school/overrides/user.py index d3b095ab..25bd819c 100644 --- a/school/overrides/user.py +++ b/school/overrides/user.py @@ -102,16 +102,6 @@ class CustomUser(User): "owner": self.name }) - def get_course_membership(self, member_type=None): - """ Returns all memberships of the user """ - filters = { - "member": self.name - } - if member_type: - filters["member_type"] = member_type - - return frappe.get_all("LMS Batch Membership", filters, ["name", "course", "progress"]) - def get_mentored_courses(self): """ Returns all courses mentored by this user """ mentored_courses = [] @@ -130,31 +120,53 @@ class CustomUser(User): return mentored_courses - def get_enrolled_courses(self): - in_progress = [] - completed = [] - memberships = self.get_course_membership("Student") - for membership in memberships: - course = frappe.db.get_value("LMS Course", membership.course, - ["name", "upcoming", "title", "image", "enable_certification"], as_dict=True) - progress = cint(membership.progress) - if progress < 100: - in_progress.append(course) - else: - completed.append(course) - return { - "in_progress": in_progress, - "completed": completed - } -def get_authored_courses(member): +def get_enrolled_courses(): + in_progress = [] + completed = [] + memberships = get_course_membership(frappe.session.user, member_type="Student") + for membership in memberships: + course = frappe.db.get_value("LMS Course", membership.course, + ["name", "upcoming", "title", "image", "enable_certification"], as_dict=True) + progress = cint(membership.progress) + if progress < 100: + in_progress.append(course) + else: + completed.append(course) + + return { + "in_progress": in_progress, + "completed": completed + } + +def get_course_membership(member, member_type=None): + """ Returns all memberships of the user """ + filters = { + "member": member + } + if member_type: + filters["member_type"] = member_type + + return frappe.get_all("LMS Batch Membership", filters, ["name", "course", "progress"]) + + +def get_authored_courses(member, only_published=True): """Returns the number of courses authored by this user. """ - return frappe.get_all( - 'LMS Course', { - 'instructor': member, - 'is_published': True - }) + course_details = [] + + filters = { + "instructor": member + } + if only_published: + filters["is_published"] = True + courses = frappe.get_all('LMS Course', filters) + + for course in courses: + course_details.append(frappe.db.get_value("LMS Course", course, + ["name", "upcoming", "title", "image", "enable_certification", "status"], as_dict=True)) + + return course_details def get_palette(full_name): """ diff --git a/school/patches.txt b/school/patches.txt index 996ddade..53e9fff4 100644 --- a/school/patches.txt +++ b/school/patches.txt @@ -22,3 +22,5 @@ school.patches.v0_0.add_progress_to_membership #20-10-2021 execute:frappe.delete_doc("Workspace", "LMS", ignore_missing=True, force=True) #24-10-2021 execute:frappe.delete_doc("Custom Field", "User-verify_age", ignore_missing=True, force=True) school.patches.v0_0.multiple_instructors #11-02-2022 +school.patches.v0_0.set_course_in_lesson #14-03-2022 +school.patches.v0_0.set_status_in_course diff --git a/school/patches/v0_0/set_course_in_lesson.py b/school/patches/v0_0/set_course_in_lesson.py new file mode 100644 index 00000000..3825b330 --- /dev/null +++ b/school/patches/v0_0/set_course_in_lesson.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + lessons = frappe.get_all("Course Lesson", fields=["name", "chapter"]) + for lesson in lessons: + course = frappe.db.get_value("Course Chapter", lesson.chapter, "course") + frappe.db.set_value("Course Lesson", lesson.name, "course", course) diff --git a/school/patches/v0_0/set_status_in_course.py b/school/patches/v0_0/set_status_in_course.py new file mode 100644 index 00000000..e8f54c1a --- /dev/null +++ b/school/patches/v0_0/set_status_in_course.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + courses = frappe.get_all("LMS Course", {"status": ("is", "not set")}, ["name", "is_published"]) + for course in courses: + status = "Approved" if course.is_published else "In Progress" + frappe.db.set_value("LMS Course", course.name, "status", status) diff --git a/school/public/css/style.css b/school/public/css/style.css index 8e4a5076..eedc201a 100644 --- a/school/public/css/style.css +++ b/school/public/css/style.css @@ -73,7 +73,7 @@ input[type=checkbox] { } .common-page-style { - padding-bottom: 5rem; + padding: 2rem 0 5rem; min-height: 60vh; padding-top: 3rem; background-color: var(--bg-color); @@ -343,7 +343,7 @@ input[type=checkbox] { .is-secondary { background: #FFFFFF; - color: inherit; + color: var(--gray-900); } .is-secondary:hover { @@ -353,7 +353,7 @@ input[type=checkbox] { .is-default { background: var(--gray-100); - color: var(--gray-700); + color: var(--gray-900); } .is-default:disabled { @@ -1384,6 +1384,28 @@ pre { overflow: inherit; } +.dashboard .nav-link { + color: var(--text-muted); + padding: 0 0 var(--padding-md); + margin-right: var(--margin-xl); +} + +.dashboard .nav-link.active { + font-weight: 600; + border-bottom: 1px solid var(--primary); + color: var(--text-color); +} + +.dashboard .nav-link:hover { + color: inherit; +} + +.dashboard-button { + position: relative; + top: -50px; + margin-left: auto; +} + .course-card-wide .breadcrumb { margin-bottom: 0; } diff --git a/school/public/icons/location.svg b/school/public/icons/location.svg deleted file mode 100644 index d062c208..00000000 --- a/school/public/icons/location.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/school/public/js/profile.js b/school/public/js/profile.js index b0aa3ccc..da0a5b9b 100644 --- a/school/public/js/profile.js +++ b/school/public/js/profile.js @@ -1,10 +1,10 @@ frappe.ready(() => { - hide_profile_for_guest_users(); + hide_profile_and_dashboard_for_guest_users(); }); -const hide_profile_for_guest_users = () => { +const hide_profile_and_dashboard_for_guest_users = () => { if (frappe.session.user == "Guest") { - var link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile"); - link_array.length && $(link_array[0]).addClass("hide"); + let links = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile" || $(elem).text().trim() === "Dashboard"); + links.length && links.each((i, elem) => $(elem).addClass("hide")); } }; diff --git a/school/templates/courses_created.html b/school/templates/courses_created.html new file mode 100644 index 00000000..5977fee0 --- /dev/null +++ b/school/templates/courses_created.html @@ -0,0 +1,23 @@ +{% set courses = get_authored_courses(frappe.session.user, only_published=False) %} +{% if courses | length %} +
+ {% for course in courses %} + {{ widgets.CourseCard(course=course) }} + {% endfor %} +
+{% else %} +
+
+ +
+
+
{{ _("No courses created") }}
+
{{ _("Help others learn something new.") }}
+
+
+ + {{ _("Create a Course") }} + +
+
+{% endif %} diff --git a/school/www/batch/learn.html b/school/www/batch/learn.html index 5e14b9da..40796629 100644 --- a/school/www/batch/learn.html +++ b/school/www/batch/learn.html @@ -52,10 +52,13 @@ {% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}" data-course="{{ course.name }}">{{ lesson.title }} COMPLETED + + {% if is_instructor %} + {{ _("Edit") }} + {% endif %}
- {% set instructors = instructors %} {% set ins_len = instructors | length %} {% for instructor in instructors %} {% if ins_len > 1 and loop.index == 1 %} diff --git a/school/www/courses/course.html b/school/www/courses/course.html index f3b7a108..405586b5 100644 --- a/school/www/courses/course.html +++ b/school/www/courses/course.html @@ -1,10 +1,12 @@ {% extends "templates/base.html" %} {% block title %}{{ course.title }} {% endblock %} + {% block head_include %} {% include "public/icons/symbol-defs.svg" %} {% endblock %} + {% block content %}
@@ -132,10 +134,30 @@
{{ course.title }}
- You have opted to be notified for this course. You will receive an email when the course becomes available. + {{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }}
-
+ {% if course.status == "Under Review" %} +
+ {{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }} +
+ {% endif %} + +
+ {% if is_instructor(course.name) %} + {{ _("Edit") }} + + + {{ _("Add Chapter") }} + + + + {{ _("Add Lesson") }} + + {% endif %} +
+ +
@@ -155,35 +177,45 @@ {% endif %}
- {% if not course.disable_self_learning and not membership and not course.upcoming and not restriction.restrict %} -
+ {% set lesson_index = get_lesson_index(membership.current_lesson) if membership and + membership.current_lesson + else '1.1' %} + + {% if show_start_learing_cta %} +
{{ _("Start Learning") }}
- {% endif %} - {% if membership %} - {% set lesson_index = get_lesson_index(membership.current_lesson) if membership and - membership.current_lesson - else '1.1' %} + {% elif is_instructor(course.name) and not course.is_published and course.status != "Under Review" %} +
+ {{ _("Submit for Review") }} + +
+ + {% elif is_instructor(course.name) %} - Continue Learning + {{ _("Checkout Course") }} - {% endif %} - {% if course.upcoming and not is_user_interested %} + {% elif membership %} + + {{ _("Continue Learning") }} + + + {% elif course.upcoming and not is_user_interested %}
- Notify me when available + {{ _("Notify me when available") }}
- {% endif %} - {% if is_cohort_staff(course.name, frappe.session.user) %} + {% elif is_cohort_staff(course.name, frappe.session.user) %} - Manage the course + {{ _("Manage the course") }} {% endif %} @@ -247,12 +279,10 @@ {% endif %} diff --git a/school/www/courses/course.js b/school/www/courses/course.js index 80068c4a..67447feb 100644 --- a/school/www/courses/course.js +++ b/school/www/courses/course.js @@ -41,6 +41,10 @@ frappe.ready(() => { create_certificate(e); }); + $("#submit-for-review").click((e) => { + submit_for_review(e); + }); + $(document).scroll(function() { let timer; clearTimeout(timer); @@ -237,7 +241,7 @@ const create_certificate = (e) => { const element_not_in_viewport = (el) => { const rect = el.getBoundingClientRect(); return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight; -} +}; const handle_overlay_display = () => { const element = $(".related-courses").length && $(".related-courses")[0]; @@ -255,4 +259,23 @@ const handle_overlay_display = () => { "bottom": "5%" }); } +}; + +const submit_for_review = (e) => { + let course = $(e.currentTarget).data("course"); + frappe.call({ + method: "school.lms.doctype.lms_course.lms_course.submit_for_review", + args: { + "course": course + }, + callback: (data) => { + if (data.message == "No Chp") { + frappe.msgprint(__(`There are no chapters in this course. + Please add chapters and lessons to your course before you submit it for review.`)); + } else if (data.message == "OK") { + frappe.msgprint(__("Your course has been submitted for review.")) + window.location.reload(); + } + } + }) } diff --git a/school/www/courses/course.py b/school/www/courses/course.py index 2a55baae..b8bfc35a 100644 --- a/school/www/courses/course.py +++ b/school/www/courses/course.py @@ -1,6 +1,6 @@ import frappe from school.lms.doctype.lms_settings.lms_settings import check_profile_restriction -from school.lms.utils import get_membership +from school.lms.utils import get_membership, is_instructor def get_context(context): context.no_cache = 1 @@ -13,7 +13,7 @@ def get_context(context): course = frappe.db.get_value("LMS Course", course_name, ["name", "title", "image", "short_introduction", "description", "is_published", "upcoming", - "disable_self_learning", "video_link", "enable_certification"], + "disable_self_learning", "video_link", "enable_certification", "status"], as_dict=True) related_courses = frappe.get_all("Related Courses", {"parent": course.name}, ["course"]) @@ -33,6 +33,7 @@ def get_context(context): if context.course.upcoming: context.is_user_interested = get_user_interest(context.course.name) context.restriction = check_profile_restriction() + context.show_start_learing_cta = show_start_learing_cta(course, membership, context.restriction) context.metatags = { "title": course.title, "image": course.image, @@ -46,3 +47,6 @@ def get_user_interest(course): "course": course, "user": frappe.session.user }) + +def show_start_learing_cta(course, membership, restriction): + return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name) diff --git a/school/www/dashboard/__init__.py b/school/www/dashboard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/www/dashboard/index.html b/school/www/dashboard/index.html new file mode 100644 index 00000000..c643915d --- /dev/null +++ b/school/www/dashboard/index.html @@ -0,0 +1,49 @@ +{% extends "templates/base.html" %} +{% block title %}{{ _("Dashboard")}} +{% endblock %} + +{% block content %} +{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %} +
+
+ {% if portal_course_creation %} + + + {{ _("New Course")}} + + {% endif %} + +
+
+
+ {% include "school/lms/web_template/courses_enrolled/courses_enrolled.html" %} +
+ {% if portal_course_creation %} +
+ {% include "school/templates/courses_created.html" %} +
+ {% endif %} +
+
+
+ + + +{% endblock %} diff --git a/school/www/profiles/profile.html b/school/www/profiles/profile.html index 96bcc9fb..ed661462 100644 --- a/school/www/profiles/profile.html +++ b/school/www/profiles/profile.html @@ -29,7 +29,7 @@ {% macro ProfileBanner(member) %} {% set cover_image = member.cover_image if member.cover_image else "/assets/school/images/profile-banner.png" %} -{% set enrollment = member.get_course_membership("Student") | length %} +{% set enrollment = get_course_membership(frappe.session.user, member_type="Student") | length %} {% set enrollment_suffix = _("Courses") if enrollment > 1 else _("Course") %}
@@ -77,9 +77,7 @@
{{ _("Courses Created") }}
{% for course in authored_courses %} - {% set course_details = frappe.db.get_value("LMS Course", course, - ["name", "upcoming", "title", "image", "enable_certification"], as_dict=True) %} - {{ widgets.CourseCard(course=course_details, read_only=read_only) }} + {{ widgets.CourseCard(course=course, read_only=read_only) }} {% endfor %}
@@ -100,7 +98,7 @@ {% endmacro %} {% macro CoursesEnrolled(member, read_only) %} -{% set enrolled = member.get_enrolled_courses() %} +{% set enrolled = get_enrolled_courses() %} {% if enrolled.completed | length %}