diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index 96e593d0..9ff38e84 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -92,7 +92,17 @@ def save_progress(lesson, course, status): "LMS Batch Membership", {"member": frappe.session.user, "course": course} ) if not membership: - return + return 0 + + body = frappe.db.get_value("Course Lesson", lesson, "body") + macros = find_macros(body) + quizzes = [value for name, value in macros if name == "Quiz"] + + for quiz in quizzes: + if not frappe.db.exists( + "LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user} + ): + return 0 filters = {"lesson": lesson, "owner": frappe.session.user, "course": course} if frappe.db.exists("LMS Course Progress", filters): diff --git a/lms/lms/doctype/lms_class/lms_class.js b/lms/lms/doctype/lms_class/lms_class.js index d3d3e2a2..7f3c4299 100644 --- a/lms/lms/doctype/lms_class/lms_class.js +++ b/lms/lms/doctype/lms_class/lms_class.js @@ -11,4 +11,24 @@ frappe.ui.form.on("LMS Class", { }; }); }, + + fetch_lessons: (frm) => { + frm.clear_table("scheduled_flow"); + frappe.call({ + method: "lms.lms.doctype.lms_class.lms_class.fetch_lessons", + args: { + courses: frm.doc.courses, + }, + callback: (r) => { + if (r.message) { + r.message.forEach((lesson) => { + let row = frm.add_child("scheduled_flow"); + row.lesson = lesson.name; + row.lesson_title = lesson.title; + }); + frm.refresh_field("scheduled_flow"); + } + }, + }); + }, }); diff --git a/lms/lms/doctype/lms_class/lms_class.json b/lms/lms/doctype/lms_class/lms_class.json index 7253100b..dd789b13 100644 --- a/lms/lms/doctype/lms_class/lms_class.json +++ b/lms/lms/doctype/lms_class/lms_class.json @@ -12,8 +12,8 @@ "start_date", "end_date", "column_break_4", - "end_time", "start_time", + "end_time", "section_break_rgfj", "medium", "category", @@ -24,9 +24,13 @@ "description", "students", "courses", + "section_break_ubxi", "custom_component", "assessment_tab", - "assessment" + "assessment", + "schedule_tab", + "fetch_lessons", + "scheduled_flow" ], "fields": [ { @@ -134,11 +138,31 @@ "fieldname": "category", "fieldtype": "Autocomplete", "label": "Category" + }, + { + "fieldname": "scheduled_flow", + "fieldtype": "Table", + "label": "Scheduled Flow", + "options": "Scheduled Flow" + }, + { + "fieldname": "section_break_ubxi", + "fieldtype": "Section Break" + }, + { + "fieldname": "fetch_lessons", + "fieldtype": "Button", + "label": "Fetch Lessons" + }, + { + "fieldname": "schedule_tab", + "fieldtype": "Tab Break", + "label": "Schedule" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-13 11:30:09.097605", + "modified": "2023-08-10 12:54:44.351907", "modified_by": "Administrator", "module": "LMS", "name": "LMS Class", diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_class/lms_class.py index 862bec75..b625d01b 100644 --- a/lms/lms/doctype/lms_class/lms_class.py +++ b/lms/lms/doctype/lms_class/lms_class.py @@ -8,6 +8,7 @@ import json from frappe import _ from frappe.model.document import Document from frappe.utils import cint, format_date, format_datetime +from lms.lms.utils import get_lessons class LMSClass(Document): @@ -18,6 +19,7 @@ class LMSClass(Document): self.validate_duplicate_students() self.validate_duplicate_assessments() self.validate_membership() + self.validate_schedule() def validate_duplicate_students(self): students = [row.student for row in self.students] @@ -66,6 +68,35 @@ class LMSClass(Document): if cint(self.seat_count) < len(self.students): frappe.throw(_("There are no seats available in this class.")) + def validate_schedule(self): + for schedule in self.scheduled_flow: + if schedule.start_time and schedule.end_time: + if ( + schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time + ): + frappe.throw( + _("Row #{0} Start time cannot be greater than or equal to end time.").format( + schedule.idx + ) + ) + + if schedule.start_time < self.start_time or schedule.start_time > self.end_time: + frappe.throw( + _("Row #{0} Start time cannot be outside the class duration.").format( + schedule.idx + ) + ) + + if schedule.end_time < self.start_time or schedule.end_time > self.end_time: + frappe.throw( + _("Row #{0} End time cannot be outside the class duration.").format(schedule.idx) + ) + + if schedule.date < self.start_date or schedule.date > self.end_date: + frappe.throw( + _("Row #{0} Date cannot be outside the class duration.").format(schedule.idx) + ) + @frappe.whitelist() def remove_student(student, class_name): @@ -188,3 +219,14 @@ def create_class( ) class_details.save() return class_details + + +@frappe.whitelist() +def fetch_lessons(courses): + lessons = [] + courses = json.loads(courses) + + for course in courses: + lessons.extend(get_lessons(course.get("course"))) + + return lessons diff --git a/lms/lms/doctype/scheduled_flow/__init__.py b/lms/lms/doctype/scheduled_flow/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/scheduled_flow/scheduled_flow.json b/lms/lms/doctype/scheduled_flow/scheduled_flow.json new file mode 100644 index 00000000..18a602b5 --- /dev/null +++ b/lms/lms/doctype/scheduled_flow/scheduled_flow.json @@ -0,0 +1,69 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-07-31 15:10:29.287475", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lesson", + "lesson_title", + "column_break_yikh", + "date", + "start_time", + "end_time" + ], + "fields": [ + { + "fieldname": "lesson", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Lesson", + "options": "Course Lesson", + "reqd": 1 + }, + { + "fieldname": "start_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "Start Time" + }, + { + "fieldname": "end_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "End Time" + }, + { + "fieldname": "column_break_yikh", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1 + }, + { + "fetch_from": "lesson.title", + "fieldname": "lesson_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Lesson Title" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-07 12:10:28.095018", + "modified_by": "Administrator", + "module": "LMS", + "name": "Scheduled Flow", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lms/lms/doctype/scheduled_flow/scheduled_flow.py b/lms/lms/doctype/scheduled_flow/scheduled_flow.py new file mode 100644 index 00000000..a712ec39 --- /dev/null +++ b/lms/lms/doctype/scheduled_flow/scheduled_flow.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ScheduledFlow(Document): + pass diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 3a2b8b1f..d472e6cf 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -143,18 +143,28 @@ def get_lesson_details(chapter): as_dict=True, ) lesson_details.number = flt(f"{chapter.idx}.{row.idx}") - lesson_details.icon = "icon-list" - macros = find_macros(lesson_details.body) + lesson_details.icon = get_lesson_icon(lesson_details.body) - for macro in macros: - if macro[0] == "YouTubeVideo" or macro[0] == "Video": - lesson_details.icon = "icon-youtube" - elif macro[0] == "Quiz": - lesson_details.icon = "icon-quiz" lessons.append(lesson_details) return lessons +def get_lesson_icon(content): + icon = None + macros = find_macros(content) + + for macro in macros: + if macro[0] == "YouTubeVideo" or macro[0] == "Video": + icon = "icon-youtube" + elif macro[0] == "Quiz": + icon = "icon-quiz" + + if not icon: + icon = "icon-list" + + return icon + + def get_tags(course): tags = frappe.db.get_value("LMS Course", course, "tags") return tags.split(",") if tags else [] @@ -272,10 +282,13 @@ def get_slugified_chapter_title(chapter): return slugify(chapter) -def get_progress(course, lesson): +def get_progress(course, lesson, member=None): + if not member: + member = frappe.session.user + return frappe.db.get_value( "LMS Course Progress", - {"course": course, "owner": frappe.session.user, "lesson": lesson}, + {"course": course, "owner": member, "lesson": lesson}, ["status"], ) @@ -343,7 +356,7 @@ def is_eligible_to_review(course, membership): def get_course_progress(course, member=None): """Returns the course progress of the session user""" - lesson_count = len(get_lessons(course)) + lesson_count = get_lessons(course, get_details=False) if not lesson_count: return 0 completed_lessons = frappe.db.count( diff --git a/lms/lms/widgets/CourseCard.html b/lms/lms/widgets/CourseCard.html index 486471c9..c17422bb 100644 --- a/lms/lms/widgets/CourseCard.html +++ b/lms/lms/widgets/CourseCard.html @@ -120,7 +120,7 @@ {% else %} {% if progress != 100 and membership and not course.upcoming %} - {% set lesson_index = get_lesson_index(membership.current_lesson or "1.1") %} + {% set lesson_index = get_lesson_index(membership.current_lesson) or "1.1" %} {% set query_parameter = "?batch=" + membership.batch if membership.batch else "" %} diff --git a/lms/lms/widgets/CourseOutline.html b/lms/lms/widgets/CourseOutline.html index 1ef4059b..b95fbdba 100644 --- a/lms/lms/widgets/CourseOutline.html +++ b/lms/lms/widgets/CourseOutline.html @@ -36,7 +36,6 @@ - @@ -57,7 +56,8 @@
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %} - diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 57508a17..803fd9e0 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -58,10 +58,10 @@ body { } .rating .star-click { - --star-fill: var(--orange-500); - background: var(--gray-200); - border-radius: var(--border-radius-md); - padding: var(--padding-xs); + --star-fill: var(--orange-500); + background: var(--gray-200); + border-radius: var(--border-radius-md); + padding: var(--padding-xs); } .cta-parent { @@ -80,10 +80,10 @@ body { .field-input { border: 1px solid var(--gray-300); - border-radius: var(--border-radius-md); - padding: 0.5rem; - width: 100%; - margin-top: 0.25rem; + border-radius: var(--border-radius-md); + padding: 0.5rem; + width: 100%; + margin-top: 0.25rem; } .field-input:focus-visible { @@ -151,9 +151,9 @@ textarea.field-input { } .ce-block__content { - max-width: 100%; - padding: 0 0.5rem; - margin: 0; + max-width: 100%; + padding: 0 0.5rem; + margin: 0; } .ce-toolbar__content { @@ -206,7 +206,7 @@ textarea.field-input { } .codex-editor path { - stroke: var(--gray-800); + stroke: var(--gray-800); } .drag-handle { @@ -618,19 +618,18 @@ input[type=checkbox] { } .reviews-parent { - color: var(--gray-900); + color: var(--gray-900); } .lesson-info { - font-size: 16px; - color: var(--gray-900); - letter-spacing: -0.011em; + padding: 0.5rem; + color: var(--gray-900); + letter-spacing: -0.011em; } .lesson-links { display: flex; align-items: center; - padding: 0.5rem; color: var(--gray-900); font-size: var(--text-base); } @@ -1046,42 +1045,42 @@ pre { .certificate-parent { display: grid; - grid-template-columns: 10fr 2fr; - grid-gap: 3rem; + grid-template-columns: 10fr 2fr; + grid-gap: 3rem; } .certificate-logo { - height: 1.5rem; - margin-bottom: 4rem; + height: 1.5rem; + margin-bottom: 4rem; } .certificate-name { - font-size: 2rem; - font-weight: 500; - color: #192734; - margin-bottom: 0.25rem; + font-size: 2rem; + font-weight: 500; + color: #192734; + margin-bottom: 0.25rem; } .certificate-footer { - margin: 4rem auto 0; - width: fit-content; + margin: 4rem auto 0; + width: fit-content; } .certificate-footer-item { - color: #192734; + color: #192734; } .cursive-font { - font-family: cursive; + font-family: cursive; font-weight: 600; } .certificate-divider { - margin: 0.5rem 0; + margin: 0.5rem 0; } .certificate-expiry { - margin-left: 2rem; + margin-left: 2rem; } .column-card { @@ -2118,10 +2117,10 @@ select { .lms-card { display: flex; flex-direction: column; - border-radius: 0.75rem; - border: 1px solid var(--gray-300); + border-radius: 0.75rem; + border: 1px solid var(--gray-300); /* box-shadow: var(--shadow-sm); */ - padding: 0.5rem; + padding: 0.5rem; height: 100%; position: relative; } @@ -2202,6 +2201,12 @@ select { cursor: none; } +.schedule-header { + display: flex; + font-size: var(--text-sm); + padding: 0.5rem 0.5rem 0 0.5rem; +} + .lms-page-style .discussions-section-title { font-size: var(--text-lg); } diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index 2eeb43ae..fc5cead8 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -200,16 +200,8 @@ const expand_the_first_chapter = () => { }; const expand_the_active_chapter = () => { - /* Find anchor matching the URL for course details page */ - let selector = $( - `a[href="${decodeURIComponent(window.location.pathname)}"]` - ).parent(); - - if (!selector.length) { - selector = $( - `a[href^="${decodeURIComponent(window.location.pathname)}"]` - ).parent(); - } + let selector = $(".course-home-headings.title"); + console.log(selector); if (selector.length && $(".course-details-page").length) { expand_for_course_details(selector); } else if ($(".active-lesson").length) { @@ -225,15 +217,11 @@ const expand_the_active_chapter = () => { const expand_for_course_details = (selector) => { $(".lesson-info").removeClass("active-lesson"); $(".lesson-info").each((i, elem) => { - let href = $(elem).find("use").attr("href"); - href.endsWith("blue") && - $(elem) - .find("use") - .attr("href", href.substring(0, href.length - 5)); + if ($(elem).data("lesson") == selector.data("lesson")) { + $(elem).addClass("active-lesson"); + show_section($(elem).parent().parent()); + } }); - selector.addClass("active-lesson"); - - show_section(selector.parent().parent()); }; const show_section = (element) => { diff --git a/lms/templates/quiz/quiz.js b/lms/templates/quiz/quiz.js index 36c2ae08..1de0b05a 100644 --- a/lms/templates/quiz/quiz.js +++ b/lms/templates/quiz/quiz.js @@ -142,6 +142,9 @@ const quiz_summary = (e = undefined) => { $("#try-again").attr("data-submission", data.message.submission); $("#try-again").removeClass("hide"); self.quiz_submitted = true; + if (this.hasOwnProperty("marked_as_complete")) { + mark_progress(); + } }, }); }; diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index 285fc2d8..dd0ae722 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -1,6 +1,7 @@ frappe.ready(() => { frappe.telemetry.capture("on_lesson_creation_page", "lms"); let self = this; + this.quiz_in_lesson = []; if ($("#current-lesson-content").length) { parse_string_to_lesson(); } @@ -48,7 +49,6 @@ const setup_editor = () => { const parse_string_to_lesson = () => { let lesson_content = $("#current-lesson-content").html(); let lesson_blocks = []; - this.quiz_in_lesson = []; lesson_content.split("\n").forEach((block) => { if (block.includes("{{ YouTubeVideo")) { diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 9c582d68..ee7fd13d 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -39,13 +39,14 @@ {% endif %}
- {{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True) }} + {% set classname = class_info.name if class_info else False %} + {{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True, classname=classname) }}
- {{ BreadCrumb(course, lesson) }} - {{ LessonContent(lesson) }} - {% if course.status == "Approved" and not course.upcoming %} + {{ BreadCrumb(course, lesson, class_info) }} + {{ LessonContent(lesson, class_info) }} + {% if course.status == "Approved" and not course.upcoming and not class_info %} {{ Discussions() }} {% endif %}
@@ -56,19 +57,39 @@ -{% macro BreadCrumb(course, lesson) %} +{% macro BreadCrumb(course, lesson, class_info) %}
{% endmacro %} -{% macro LessonContent(lesson) %} +{% macro LessonContent(lesson, class_info) %} {% set instructors = get_instructors(course.name) %} {% set is_instructor = is_instructor(course.name) %} @@ -146,8 +167,9 @@ {% endif %} + {% if not class_info %} {{ pagination(prev_url, next_url) }} - + {% endif %} {% endmacro %} diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 3b1554f3..f7cf6a94 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -61,8 +61,10 @@ const mark_progress = () => { status: status, }, callback: (data) => { - change_progress_indicators(); - show_certificate_if_course_completed(data); + if (data.message) { + change_progress_indicators(); + show_certificate_if_course_completed(data); + } }, }); }; diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index 077b3ba1..fae056f7 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -15,6 +15,16 @@ def get_context(context): chapter_index = frappe.form_dict.get("chapter") lesson_index = frappe.form_dict.get("lesson") + class_name = frappe.form_dict.get("class") + + if class_name: + context.class_info = frappe._dict( + { + "name": class_name, + "title": frappe.db.get_value("LMS Class", class_name, "title"), + } + ) + lesson_number = f"{chapter_index}.{lesson_index}" context.lesson_number = lesson_number context.lesson_index = lesson_index diff --git a/lms/www/classes/class.html b/lms/www/classes/class.html index 336c7b91..8726da4b 100644 --- a/lms/www/classes/class.html +++ b/lms/www/classes/class.html @@ -10,7 +10,7 @@ {{ BreadCrumb(class_info) }}
{{ ClassDetails(class_info) }} - {{ ClassSections(class_info, class_courses, class_students) }} + {{ ClassSections(class_info, class_courses, class_students, flow) }}
@@ -82,7 +82,7 @@ -{% macro ClassSections(class_info, class_courses, class_students) %} +{% macro ClassSections(class_info, class_courses, class_students, flow) %}
{% if is_moderator %} @@ -110,6 +110,17 @@ + {% if flow | length %} + + {% endif %} +
+ {% if flow | length %} +
+ {{ ScheduleSection(flow) }} +
+ {% endif %} +
{{ StudentsSection(class_info, class_students) }}
@@ -510,6 +527,83 @@ {% endmacro %} + +{% macro ScheduleSection(flow) %} +
+
+
+ {{ _("Schedule") }} +
+
+ +
+ {% for chapter in flow %} +
+
+ +
+ {{ chapter.chapter_title }} +
+
+
+
+ {% endfor %} +
+
+{% endmacro %} + {%- block script %} {{ super() }} {% if is_moderator %} diff --git a/lms/www/classes/class.py b/lms/www/classes/class.py index dfab4810..589d6db0 100644 --- a/lms/www/classes/class.py +++ b/lms/www/classes/class.py @@ -8,6 +8,9 @@ from lms.lms.utils import ( get_upcoming_evals, has_submitted_assessment, has_graded_assessment, + get_lesson_index, + get_lesson_url, + get_lesson_icon, get_membership, ) @@ -54,16 +57,9 @@ def get_context(context): order_by="creation desc", ) - context.live_classes = frappe.get_all( - "LMS Live Class", - {"class_name": class_name, "date": [">=", getdate()]}, - ["title", "description", "time", "date", "start_url", "join_url", "owner"], - order_by="date", - ) - context.class_courses = get_class_course_details(class_courses) context.course_list = [course.course for course in context.class_courses] - context.all_courses = frappe.get_list( + context.all_courses = frappe.get_all( "LMS Course", fields=["name", "title"], limit_page_length=0 ) context.course_name_list = [course.course for course in context.class_courses] @@ -73,11 +69,22 @@ def get_context(context): ) context.is_student = is_student(class_students) + if not context.is_student and not context.is_moderator and not context.is_evaluator: + raise frappe.PermissionError(_("You don't have permission to access this page.")) + + context.live_classes = frappe.get_all( + "LMS Live Class", + {"class_name": class_name, "date": [">=", getdate()]}, + ["title", "description", "time", "date", "start_url", "join_url", "owner"], + order_by="date", + ) + context.current_student = ( get_current_student_details(class_courses, class_name) if context.is_student else None ) context.all_assignments = get_all_assignments(class_name) context.all_quizzes = get_all_quizzes(class_name) + context.flow = get_scheduled_flow(class_name) def get_all_quizzes(class_name): @@ -203,6 +210,53 @@ def is_student(class_students): return frappe.session.user in students +def get_scheduled_flow(class_name): + chapters = [] + + lessons = frappe.get_all( + "Scheduled Flow", + {"parent": class_name}, + ["name", "lesson", "date", "start_time", "end_time"], + order_by="idx", + ) + + for lesson in lessons: + lesson = get_lesson_details(lesson, class_name) + chapter_exists = [ + chapter for chapter in chapters if chapter.chapter == lesson.chapter + ] + + if len(chapter_exists) == 0: + chapters.append( + frappe._dict( + { + "chapter": lesson.chapter, + "chapter_title": frappe.db.get_value("Course Chapter", lesson.chapter, "title"), + "lessons": [lesson], + } + ) + ) + else: + chapter_exists[0]["lessons"].append(lesson) + + return chapters + + +def get_lesson_details(lesson, class_name): + lesson.update( + frappe.db.get_value( + "Course Lesson", + lesson.lesson, + ["name", "title", "body", "course", "chapter"], + as_dict=True, + ) + ) + lesson.index = get_lesson_index(lesson.lesson) + lesson.url = get_lesson_url(lesson.course, lesson.index) + "?class=" + class_name + lesson.icon = get_lesson_icon(lesson.body) + return lesson + + def get_current_student_details(class_courses, class_name): student_details = frappe._dict() student_details.courses = frappe._dict()