From 625ddac65a5d53cfb88aa643ce4da255d42eb206 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 14 Aug 2025 20:26:46 +0530 Subject: [PATCH] refactor: learning path --- frontend/components.d.ts | 1 + frontend/src/components/AppSidebar.vue | 30 +- frontend/src/components/BatchOverlay.vue | 16 +- frontend/src/components/CourseCard.vue | 4 +- frontend/src/components/Settings/Members.vue | 10 +- frontend/src/components/Settings/Settings.vue | 7 - frontend/src/components/SidebarLink.vue | 2 +- frontend/src/pages/ProgramForm.vue | 379 ------------- frontend/src/pages/Programs.vue | 216 ------- frontend/src/pages/Programs/ProgramForm.vue | 528 ++++++++++++++++++ frontend/src/pages/Programs/Programs.vue | 103 ++++ frontend/src/pages/Programs/types.ts | 51 ++ frontend/src/router.js | 8 +- frontend/src/stores/settings.js | 8 - frontend/src/stores/sidebar.js | 1 + frontend/src/stores/user.js | 5 +- lms/lms/doctype/lms_program/lms_program.json | 68 ++- .../lms_program_course.json | 8 +- .../lms_program_member.json | 8 +- .../doctype/lms_settings/lms_settings.json | 9 +- lms/lms/utils.py | 4 - 21 files changed, 785 insertions(+), 681 deletions(-) delete mode 100644 frontend/src/pages/ProgramForm.vue delete mode 100644 frontend/src/pages/Programs.vue create mode 100644 frontend/src/pages/Programs/ProgramForm.vue create mode 100644 frontend/src/pages/Programs/Programs.vue create mode 100644 frontend/src/pages/Programs/types.ts diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 88e57b82..cd85de64 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -88,6 +88,7 @@ declare module 'vue' { PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default'] Play: typeof import('./src/components/Icons/Play.vue')['default'] + ProgramForm: typeof import('./src/components/Modals/ProgramForm.vue')['default'] ProgressBar: typeof import('./src/components/ProgressBar.vue')['default'] Question: typeof import('./src/components/Modals/Question.vue')['default'] Quiz: typeof import('./src/components/Quiz.vue')['default'] diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index f1421537..c76576b9 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -361,34 +361,20 @@ const addProgrammingExercises = () => { } const addPrograms = () => { - let activeFor = ['Programs', 'ProgramForm'] + let activeFor = ['Programs'] let index = 1 - let canAddProgram = false - if ( - !isInstructor.value && - !isModerator.value && - settingsStore.learningPaths.data - ) { - sidebarLinks.value = sidebarLinks.value.filter( - (link) => link.label !== 'Courses' - ) + if (!isInstructor.value && !isModerator.value) { activeFor.push('CourseDetail') activeFor.push('Lesson') - index = 0 - canAddProgram = true - } else if (isInstructor.value || isModerator.value) { - canAddProgram = true } - if (canAddProgram) { - sidebarLinks.value.splice(index, 0, { - label: 'Programs', - icon: 'Route', - to: 'Programs', - activeFor: activeFor, - }) - } + sidebarLinks.value.splice(index, 0, { + label: 'Programs', + icon: 'Route', + to: 'Programs', + activeFor: activeFor, + }) } const openPageModal = (link) => { diff --git a/frontend/src/components/BatchOverlay.vue b/frontend/src/components/BatchOverlay.vue index fdd3a893..cc846d37 100644 --- a/frontend/src/components/BatchOverlay.vue +++ b/frontend/src/components/BatchOverlay.vue @@ -56,7 +56,7 @@
- {{ isModerator ? __('Manage Batch') : __('Visit Batch') }} + {{ isStudent ? __('Visit Batch') : __('Manage Batch') }} @@ -204,4 +204,12 @@ const isStudent = computed(() => { const isModerator = computed(() => { return user.data?.is_moderator }) + +const isEvaluator = computed(() => { + return user.data?.is_evaluator +}) + +const canAccessBatch = computed(() => { + return isModerator.value || isStudent.value || isEvaluator.value +}) diff --git a/frontend/src/components/CourseCard.vue b/frontend/src/components/CourseCard.vue index 00b13bb1..c1740b46 100644 --- a/frontend/src/components/CourseCard.vue +++ b/frontend/src/components/CourseCard.vue @@ -1,7 +1,7 @@ diff --git a/frontend/src/pages/Programs.vue b/frontend/src/pages/Programs.vue deleted file mode 100644 index 15cd828a..00000000 --- a/frontend/src/pages/Programs.vue +++ /dev/null @@ -1,216 +0,0 @@ - - diff --git a/frontend/src/pages/Programs/ProgramForm.vue b/frontend/src/pages/Programs/ProgramForm.vue new file mode 100644 index 00000000..02ca95ce --- /dev/null +++ b/frontend/src/pages/Programs/ProgramForm.vue @@ -0,0 +1,528 @@ + + diff --git a/frontend/src/pages/Programs/Programs.vue b/frontend/src/pages/Programs/Programs.vue new file mode 100644 index 00000000..dad58e58 --- /dev/null +++ b/frontend/src/pages/Programs/Programs.vue @@ -0,0 +1,103 @@ + + diff --git a/frontend/src/pages/Programs/types.ts b/frontend/src/pages/Programs/types.ts new file mode 100644 index 00000000..2b352c27 --- /dev/null +++ b/frontend/src/pages/Programs/types.ts @@ -0,0 +1,51 @@ +interface Program { + name: string; + title: string; + published: boolean; + enforce_course_order: boolean; + allow_self_enrollment: boolean; + program_courses: ProgramCourse[]; + program_batches: ProgramMember[]; + course_count: number; + member_count: number; +} + +interface ProgramCourse { + course: string; + course_title: string; + idx: number; + name: string; +} + +interface ProgramMember { + member: string; + full_name: string; + progress: number; + idx: number; + name: string; +} + +interface Programs { + data: Program[]; + reload: () => void; + hasNextPage: boolean; + next: () => void; + setValue: { + submit: ( + data: Program, + options?: { onSuccess?: () => void } + ) => void; + }; + insert: { + submit: ( + data: Program, + options?: { onSuccess?: () => void } + ) => void; + }; + delete: { + submit: ( + name: string, + options?: { onSuccess?: () => void } + ) => void; + }; +} \ No newline at end of file diff --git a/frontend/src/router.js b/frontend/src/router.js index c9db6b26..bab1d69b 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -183,16 +183,10 @@ const routes = [ component: () => import('@/pages/QuizSubmission.vue'), props: true, }, - { - path: '/programs/:programName', - name: 'ProgramForm', - component: () => import('@/pages/ProgramForm.vue'), - props: true, - }, { path: '/programs', name: 'Programs', - component: () => import('@/pages/Programs.vue'), + component: () => import('@/pages/Programs/Programs.vue'), }, { path: '/assignments', diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js index b323ef0f..68c3cd06 100644 --- a/frontend/src/stores/settings.js +++ b/frontend/src/stores/settings.js @@ -8,13 +8,6 @@ export const useSettings = defineStore('settings', () => { const isSettingsOpen = ref(false) const activeTab = ref(null) - const learningPaths = createResource({ - url: 'lms.lms.api.get_lms_setting', - params: { field: 'enable_learning_paths' }, - auto: true, - cache: ['learningPath'], - }) - const allowGuestAccess = createResource({ url: 'lms.lms.api.get_lms_setting', params: { field: 'allow_guest_access' }, @@ -38,7 +31,6 @@ export const useSettings = defineStore('settings', () => { return { isSettingsOpen, activeTab, - learningPaths, allowGuestAccess, preventSkippingVideos, sidebarSettings, diff --git a/frontend/src/stores/sidebar.js b/frontend/src/stores/sidebar.js index f7faa9f5..734d3485 100644 --- a/frontend/src/stores/sidebar.js +++ b/frontend/src/stores/sidebar.js @@ -4,6 +4,7 @@ import { ref } from 'vue' export const useSidebar = defineStore('sidebar', () => { const isSidebarCollapsed = ref(false) const isWebpagesCollapsed = ref(true) + const canAccessPrograms = ref(false) if (localStorage.getItem('isSidebarCollapsed')) { isSidebarCollapsed.value = JSON.parse( diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index cb744ab5..1148bd29 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -1,15 +1,12 @@ import { defineStore } from 'pinia' import { createResource } from 'frappe-ui' -import { useRouter } from 'vue-router' - -const router = useRouter() export const usersStore = defineStore('lms-users', () => { let userResource = createResource({ url: 'lms.lms.api.get_user_info', onError(error) { if (error && error.exc_type === 'AuthenticationError') { - router.push('/login') + window.location.href = '/login' } }, auto: true, diff --git a/lms/lms/doctype/lms_program/lms_program.json b/lms/lms/doctype/lms_program/lms_program.json index f92c689a..0684f481 100644 --- a/lms/lms/doctype/lms_program/lms_program.json +++ b/lms/lms/doctype/lms_program/lms_program.json @@ -6,9 +6,19 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "published", "title", + "column_break_cwjx", + "enforce_course_order", + "column_break_mikl", + "allow_self_enrollment", + "section_break_vhhu", "program_courses", - "program_members" + "program_members", + "section_break_pppe", + "course_count", + "column_break_qwhf", + "member_count" ], "fields": [ { @@ -30,12 +40,61 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Published" + }, + { + "default": "0", + "fieldname": "enforce_course_order", + "fieldtype": "Check", + "label": "Enforce Course Order" + }, + { + "fieldname": "section_break_vhhu", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_cwjx", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "allow_self_enrollment", + "fieldtype": "Check", + "label": "Allow Self Enrollment" + }, + { + "fieldname": "section_break_pppe", + "fieldtype": "Section Break" + }, + { + "fieldname": "course_count", + "fieldtype": "Int", + "label": "Course Count" + }, + { + "fieldname": "column_break_qwhf", + "fieldtype": "Column Break" + }, + { + "fieldname": "member_count", + "fieldtype": "Int", + "label": "Member Count" + }, + { + "fieldname": "column_break_mikl", + "fieldtype": "Column Break" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-11-28 22:06:16.742867", - "modified_by": "Administrator", + "modified": "2025-08-13 14:36:59.168945", + "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Program", "naming_rule": "By fieldname", @@ -78,8 +137,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/lms/lms/doctype/lms_program_course/lms_program_course.json b/lms/lms/doctype/lms_program_course/lms_program_course.json index a7edc7ed..d504946d 100644 --- a/lms/lms/doctype/lms_program_course/lms_program_course.json +++ b/lms/lms/doctype/lms_program_course/lms_program_course.json @@ -27,16 +27,18 @@ "read_only": 1 } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-11-18 12:43:46.800199", - "modified_by": "Administrator", + "modified": "2025-08-13 17:32:43.554055", + "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Program Course", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/lms/lms/doctype/lms_program_member/lms_program_member.json b/lms/lms/doctype/lms_program_member/lms_program_member.json index f629e1f2..bd5d5ff0 100644 --- a/lms/lms/doctype/lms_program_member/lms_program_member.json +++ b/lms/lms/doctype/lms_program_member/lms_program_member.json @@ -35,16 +35,18 @@ "label": "Progress" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-11-21 12:51:31.882576", - "modified_by": "Administrator", + "modified": "2025-08-13 17:33:00.265037", + "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Program Member", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index 226da133..8be2517c 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -11,7 +11,6 @@ "persona_captured", "column_break_zdel", "allow_guest_access", - "enable_learning_paths", "prevent_skipping_videos", "column_break_bjis", "unsplash_access_key", @@ -339,12 +338,6 @@ "fieldtype": "HTML", "label": "Payments app is not installed" }, - { - "default": "0", - "fieldname": "enable_learning_paths", - "fieldtype": "Check", - "label": "Enable Learning Paths" - }, { "fieldname": "general_tab", "fieldtype": "Tab Break", @@ -429,7 +422,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-07-01 17:01:58.466698", + "modified": "2025-08-12 16:47:49.983018", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Settings", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index d81ff843..b39dc544 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -1921,10 +1921,6 @@ def update_certificate_purchase(course, payment_name): def get_programs(): if has_course_moderator_role() or has_course_instructor_role() or has_course_evaluator_role(): programs = frappe.get_all("LMS Program", fields=["name"]) - else: - programs = frappe.get_all( - "LMS Program Member", {"member": frappe.session.user}, ["parent as name", "progress"] - ) for program in programs: program_courses = frappe.get_all(