From a88243270255b54d8b6c808b6a22d763ee284fd5 Mon Sep 17 00:00:00 2001 From: raizasafeel <89463672+raizasafeel@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:32:37 +0530 Subject: [PATCH 01/41] test: added test for chapter deletion and renumbering --- .../course_chapter/test_course_chapter.py | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/lms/lms/doctype/course_chapter/test_course_chapter.py b/lms/lms/doctype/course_chapter/test_course_chapter.py index ebeda074..a4c9942a 100644 --- a/lms/lms/doctype/course_chapter/test_course_chapter.py +++ b/lms/lms/doctype/course_chapter/test_course_chapter.py @@ -1,9 +1,39 @@ # Copyright (c) 2021, FOSS United and Contributors # See license.txt -# import frappe -import unittest +import frappe + +from lms.lms.api import delete_chapter +from lms.lms.test_helpers import BaseTestUtils -class TestCourseChapter(unittest.TestCase): - pass +class TestCourseChapter(BaseTestUtils): + def setUp(self): + super().setUp() + self.instructor = self._create_user( + "frappe@example.com", "Frappe", "Admin", ["Moderator", "Course Creator"] + ) + + def tearDown(self): + return super().tearDown() + + def test_chapter_deletion_and_renumbering(self): + course = self._create_course(f"Test Renumbering Course {frappe.generate_hash()[:8]}") + chapters = [] + + for i in range(1, 4): + chapter = self._create_chapter(f"Chapter {i}", course.name) + chapters.append(chapter) + self._create_chapter_reference(course.name, chapter.name, i) + self.assertEqual(self._get_chapter_index(course.name, chapter.name), i) + + delete_chapter(chapters[1].name) + + idx_ch1 = self._get_chapter_index(course.name, chapters[0].name) + idx_ch3 = self._get_chapter_index(course.name, chapters[2].name) + + self.assertEqual(idx_ch1, 1, "Chapter 1 index should remain 1") + self.assertEqual(idx_ch3, 2, "Chapter 3 index should be renumbered to 2 after deleting Chapter 2") + + def _get_chapter_index(self, course, chapter): + return frappe.db.get_value("Chapter Reference", {"parent": course, "chapter": chapter}, "idx") From df2f2e6603dec870e5c5d946c992e4c541a95aee Mon Sep 17 00:00:00 2001 From: raizasafeel <89463672+raizasafeel@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:21:31 +0530 Subject: [PATCH 02/41] fix(courses): use constant value for filter instead of translated label --- frontend/src/pages/Courses/Courses.vue | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/Courses/Courses.vue b/frontend/src/pages/Courses/Courses.vue index 0bd81158..f111efb3 100644 --- a/frontend/src/pages/Courses/Courses.vue +++ b/frontend/src/pages/Courses/Courses.vue @@ -147,7 +147,7 @@ const currentCategory = ref(null) const title = ref('') const certification = ref(false) const filters = ref({}) -const currentTab = ref('Live') +const currentTab = ref('live') const { brand } = sessionStore() const courseCount = ref(0) const router = useRouter() @@ -267,35 +267,35 @@ const updateTabFilter = () => { delete filters.value['published_on'] delete filters.value['upcoming'] - if (currentTab.value == 'Enrolled' && user.data?.is_student) { + if (currentTab.value == 'enrolled' && user.data?.is_student) { filters.value['enrolled'] = 1 delete filters.value['published'] } else { delete filters.value['published'] delete filters.value['enrolled'] - if (currentTab.value == 'Live') { + if (currentTab.value == 'live') { filters.value['published'] = 1 filters.value['upcoming'] = 0 filters.value['live'] = 1 - } else if (currentTab.value == 'Upcoming') { + } else if (currentTab.value == 'upcoming') { filters.value['upcoming'] = 1 - } else if (currentTab.value == 'New') { + } else if (currentTab.value == 'new') { filters.value['published'] = 1 filters.value['published_on'] = [ '>=', dayjs().add(-3, 'month').format('YYYY-MM-DD'), ] - } else if (currentTab.value == 'Created') { + } else if (currentTab.value == 'created') { filters.value['created'] = 1 - } else if (currentTab.value == 'Unpublished') { + } else if (currentTab.value == 'unpublished') { filters.value['published'] = 0 } } } const updateStudentFilter = () => { - if (!user.data || (user.data?.is_student && currentTab.value != 'Enrolled')) { + if (!user.data || (user.data?.is_student && currentTab.value != 'enrolled')) { filters.value['published'] = 1 } } @@ -345,12 +345,15 @@ const courseTabs = computed(() => { let tabs = [ { label: __('Live'), + value: 'live', }, { label: __('New'), + value: 'new', }, { label: __('Upcoming'), + value: 'upcoming', }, ] if ( @@ -358,10 +361,10 @@ const courseTabs = computed(() => { user.data?.is_instructor || user.data?.is_evaluator ) { - tabs.push({ label: __('Created') }) - tabs.push({ label: __('Unpublished') }) + tabs.push({ label: __('Created'), value: 'created' }) + tabs.push({ label: __('Unpublished'), value: 'unpublished' }) } else if (user.data) { - tabs.push({ label: __('Enrolled') }) + tabs.push({ label: __('Enrolled'), value: 'enrolled' }) } return tabs }) From be76268c7010e8aa6e8ebd5c5a6803596d7adbb5 Mon Sep 17 00:00:00 2001 From: raizasafeel <89463672+raizasafeel@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:22:37 +0530 Subject: [PATCH 03/41] fix(batches): use constant value for filter instead of translated label --- frontend/src/pages/Batches.vue | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/Batches.vue b/frontend/src/pages/Batches.vue index b9821622..43cc71de 100644 --- a/frontend/src/pages/Batches.vue +++ b/frontend/src/pages/Batches.vue @@ -155,7 +155,7 @@ const title = ref('') const certification = ref(false) const filters = ref({}) const is_student = computed(() => user.data?.is_student) -const currentTab = ref(is_student.value ? 'All' : 'Upcoming') +const currentTab = ref(is_student.value ? 'all' : 'upcoming') const orderBy = ref('start_date') const readOnlyMode = window.read_only_mode const router = useRouter() @@ -245,7 +245,7 @@ const updateTabFilter = () => { if (!user.data) { return } - if (currentTab.value == 'Enrolled' && is_student.value) { + if (currentTab.value == 'enrolled' && is_student.value) { filters.value['enrolled'] = 1 delete filters.value['start_date'] delete filters.value['published'] @@ -256,20 +256,20 @@ const updateTabFilter = () => { delete filters.value['start_date'] delete filters.value['published'] orderBy.value = 'start_date desc' - if (currentTab.value == 'Upcoming') { + if (currentTab.value == 'upcoming') { filters.value['start_date'] = ['>=', dayjs().format('YYYY-MM-DD')] filters.value['published'] = 1 orderBy.value = 'start_date' - } else if (currentTab.value == 'Archived') { + } else if (currentTab.value == 'archived') { filters.value['start_date'] = ['<=', dayjs().format('YYYY-MM-DD')] - } else if (currentTab.value == 'Unpublished') { + } else if (currentTab.value == 'unpublished') { filters.value['published'] = 0 } } } const updateStudentFilter = () => { - if (!user.data || (is_student.value && currentTab.value != 'Enrolled')) { + if (!user.data || (is_student.value && currentTab.value != 'enrolled')) { filters.value['start_date'] = ['>=', dayjs().format('YYYY-MM-DD')] filters.value['published'] = 1 } @@ -319,6 +319,7 @@ const batchTabs = computed(() => { let tabs = [ { label: __('All'), + value: 'all', }, ] @@ -327,11 +328,11 @@ const batchTabs = computed(() => { user.data?.is_instructor || user.data?.is_evaluator ) { - tabs.push({ label: __('Upcoming') }) - tabs.push({ label: __('Archived') }) - tabs.push({ label: __('Unpublished') }) + tabs.push({ label: __('Upcoming'), value: 'upcoming' }) + tabs.push({ label: __('Archived'), value: 'archived' }) + tabs.push({ label: __('Unpublished'), value: 'unpublished' }) } else if (user.data) { - tabs.push({ label: __('Enrolled') }) + tabs.push({ label: __('Enrolled'), value: 'enrolled' }) } return tabs }) From 58b49e36086b59065ce44cb4079fd83abbd142d5 Mon Sep 17 00:00:00 2001 From: raizasafeel <89463672+raizasafeel@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:28:06 +0530 Subject: [PATCH 04/41] fix(batches): order assessments by their index --- lms/lms/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 052efbdc..4e4de890 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -1242,6 +1242,7 @@ def get_assessments(batch): "LMS Assessment", {"parent": batch}, ["name", "assessment_type", "assessment_name"], + order_by="idx", ) for assessment in assessments: From 8febe21aa8f3c1bcdb22fd8ed7e6cf898e61063d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 3 Feb 2026 10:48:59 +0530 Subject: [PATCH 05/41] fix: dont allow contact us email sending to guest users --- frontend/src/utils/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index e0a69d55..2261349c 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -513,7 +513,8 @@ const getSidebarItems = () => { : settings.data?.contact_us_email, condition: () => { return ( - settings?.data?.contact_us_email || + (settings?.data?.contact_us_email && + userResource?.data) || settings?.data?.contact_us_url ) }, From e4268d04372166adb3f97b6968565ca187794f78 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 3 Feb 2026 10:49:48 +0530 Subject: [PATCH 06/41] fix: allow attaching payment information for batch enrollment --- .../src/components/Controls/Autocomplete.vue | 21 +++-- .../src/components/Modals/StudentModal.vue | 78 ++++++++++--------- .../pages/Courses/CourseEnrollmentModal.vue | 8 +- 3 files changed, 56 insertions(+), 51 deletions(-) diff --git a/frontend/src/components/Controls/Autocomplete.vue b/frontend/src/components/Controls/Autocomplete.vue index 4da8274e..dee1268f 100644 --- a/frontend/src/components/Controls/Autocomplete.vue +++ b/frontend/src/components/Controls/Autocomplete.vue @@ -99,18 +99,17 @@ name="item-label" v-bind="{ active, selected, option }" > -
-
- {{ option.label }} +
+
+ {{ + option.value == option.label + ? option.description + : option.label + }} +
+
+ {{ option.value }}
-
diff --git a/frontend/src/components/Modals/StudentModal.vue b/frontend/src/components/Modals/StudentModal.vue index 6847b358..5c9c7b97 100644 --- a/frontend/src/components/Modals/StudentModal.vue +++ b/frontend/src/components/Modals/StudentModal.vue @@ -2,7 +2,7 @@ + diff --git a/frontend/src/pages/Courses/CourseEnrollmentModal.vue b/frontend/src/pages/Courses/CourseEnrollmentModal.vue index 7e69c589..ebc2f95d 100644 --- a/frontend/src/pages/Courses/CourseEnrollmentModal.vue +++ b/frontend/src/pages/Courses/CourseEnrollmentModal.vue @@ -19,8 +19,7 @@ placeholder=" " v-model="student" :required="true" - :allowCreate="true" - @create=" + :onCreate=" () => { openSettings('Members') show = false @@ -33,8 +32,7 @@ :label="__('Payment')" placeholder=" " v-model="payment" - :allowCreate="true" - @create=" + :onCreate=" () => { openSettings('Transactions') show = false @@ -54,9 +52,9 @@ diff --git a/frontend/src/pages/Home/StudentHome.vue b/frontend/src/pages/Home/StudentHome.vue index 40f66849..1c45e331 100644 --- a/frontend/src/pages/Home/StudentHome.vue +++ b/frontend/src/pages/Home/StudentHome.vue @@ -72,7 +72,7 @@
-
+
{{ diff --git a/lms/hooks.py b/lms/hooks.py index 26f1ce31..2a289ce1 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -3,7 +3,7 @@ import frappe from . import __version__ as app_version app_name = "frappe_lms" -app_title = "Frappe LMS" +app_title = "Learning" app_publisher = "Frappe" app_description = "Frappe LMS App" app_icon_url = "/assets/lms/images/lms-logo.png" diff --git a/lms/lms/api.py b/lms/lms/api.py index d7ac8173..9421853e 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -1654,8 +1654,12 @@ def get_progress_distribution(progressList): "value": len([p for p in progressList if 30 <= p < 60]), }, { - "name": "Advanced (60-100%)", - "value": len([p for p in progressList if 60 <= p <= 100]), + "name": "Advanced (60-99%)", + "value": len([p for p in progressList if 60 <= p < 100]), + }, + { + "name": "Completed (100%)", + "value": len([p for p in progressList if p == 100]), }, ] @@ -2048,13 +2052,17 @@ def get_lesson_completion_stats(course): Lesson = frappe.qb.DocType("Course Lesson") rows = ( - frappe.qb.from_(CourseProgress) - .join(LessonReference) - .on(CourseProgress.lesson == LessonReference.lesson) + frappe.qb.from_(LessonReference) .join(ChapterReference) .on(LessonReference.parent == ChapterReference.chapter) .join(Lesson) - .on(CourseProgress.lesson == Lesson.name) + .on(LessonReference.lesson == Lesson.name) + .left_join(CourseProgress) + .on( + (CourseProgress.lesson == LessonReference.lesson) + & (CourseProgress.course == course) + & (CourseProgress.status == "Complete") + ) .select( LessonReference.idx, ChapterReference.idx.as_("chapter_idx"), @@ -2063,10 +2071,132 @@ def get_lesson_completion_stats(course): Lesson.name.as_("lesson_name"), fn.Count(CourseProgress.name).as_("completion_count"), ) - .where((CourseProgress.course == course) & (CourseProgress.status == "Complete")) - .groupby(CourseProgress.lesson) + .where(ChapterReference.parent == course) + .groupby(LessonReference.lesson) .orderby(ChapterReference.idx, LessonReference.idx) .run(as_dict=True) ) return rows + + +@frappe.whitelist() +def get_course_assessment_progress(course, member): + if not can_modify_course(course): + frappe.throw( + _("You do not have permission to access this course's assessment data."), frappe.PermissionError + ) + + quizzes = get_course_quiz_progress(course, member) + assignments = get_course_assignment_progress(course, member) + programming_exercises = get_course_programming_exercise_progress(course, member) + + return { + "quizzes": quizzes, + "assignments": assignments, + "exercises": programming_exercises, + } + + +def get_course_quiz_progress(course, member): + quizzes = get_assessment_from_lesson(course, "quiz") + attempts = [] + + for quiz in quizzes: + submissions = frappe.get_all( + "LMS Quiz Submission", + { + "quiz": quiz, + "member": member, + }, + ["name", "score", "percentage", "quiz", "quiz_title"], + order_by="creation desc", + limit=1, + ) + if len(submissions): + attempts.append(submissions[0]) + else: + attempts.append( + { + "quiz": quiz, + "quiz_title": frappe.db.get_value("LMS Quiz", quiz, "title"), + "score": 0, + "percentage": 0, + } + ) + + return attempts + + +def get_course_assignment_progress(course, member): + assignments = get_assessment_from_lesson(course, "assignment") + submissions = [] + + for assignment in assignments: + assignment_subs = frappe.get_all( + "LMS Assignment Submission", + { + "assignment": assignment, + "member": member, + }, + ["name", "status", "assignment", "assignment_title"], + order_by="creation desc", + limit=1, + ) + if len(assignment_subs): + submissions.append(assignment_subs[0]) + else: + submissions.append( + { + "assignment": assignment, + "assignment_title": frappe.db.get_value("LMS Assignment", assignment, "title"), + "status": "Not Submitted", + } + ) + + return submissions + + +def get_course_programming_exercise_progress(course, member): + exercises = get_assessment_from_lesson(course, "program") + submissions = [] + + for exercise in exercises: + exercise_subs = frappe.get_all( + "LMS Programming Exercise Submission", + { + "exercise": exercise, + "member": member, + }, + ["name", "status", "exercise", "exercise_title"], + order_by="creation desc", + limit=1, + ) + if len(exercise_subs): + submissions.append(exercise_subs[0]) + else: + submissions.append( + { + "exercise": exercise, + "exercise_title": frappe.db.get_value("LMS Programming Exercise", exercise, "title"), + "status": "Not Attempted", + } + ) + + return submissions + + +def get_assessment_from_lesson(course, assessmentType): + assessments = [] + lessons = frappe.get_all("Course Lesson", {"course": course}, ["name", "title", "content"]) + + for lesson in lessons: + if lesson.content: + content = json.loads(lesson.content) + for block in content.get("blocks", []): + if block.get("type") == assessmentType: + data_field = "exercise" if assessmentType == "program" else assessmentType + quiz_name = block.get("data", {}).get(data_field) + assessments.append(quiz_name) + + return assessments diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 052efbdc..8c05ba82 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -205,6 +205,10 @@ def get_lesson_icon(body, content): if block.get("type") == "quiz": return "icon-quiz" + if block.get("type") == "assignment": + return "icon-assignment" + if block.get("type") == "program": + return "icon-code" return "icon-list" From 9814abf55f56fa555a5f5f34b0c63b3b1820f840 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 4 Feb 2026 16:04:28 +0530 Subject: [PATCH 09/41] fix: dark mode issues of course dashboard --- frontend/src/components/NumberChartGraph.vue | 2 +- .../src/pages/Courses/CourseDashboard.vue | 6 +-- .../pages/Courses/StudentCourseProgress.vue | 46 +++++++++++++++---- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/NumberChartGraph.vue b/frontend/src/components/NumberChartGraph.vue index 51c771c3..67fdd508 100644 --- a/frontend/src/components/NumberChartGraph.vue +++ b/frontend/src/components/NumberChartGraph.vue @@ -5,7 +5,7 @@
-
+
{{ value }}
diff --git a/frontend/src/pages/Courses/CourseDashboard.vue b/frontend/src/pages/Courses/CourseDashboard.vue index 5bb8680f..186c29f1 100644 --- a/frontend/src/pages/Courses/CourseDashboard.vue +++ b/frontend/src/pages/Courses/CourseDashboard.vue @@ -22,7 +22,7 @@
-
+
{{ __('Students') }}
@@ -132,7 +132,7 @@
-
+