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/components/Settings/Members.vue b/frontend/src/components/Settings/Members.vue
index 285a2a26..7b131688 100644
--- a/frontend/src/components/Settings/Members.vue
+++ b/frontend/src/components/Settings/Members.vue
@@ -117,14 +117,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 @@
-
-
-
-
-
-
-
-
-
- {{ program.name }}
-
-
-
- {{ program.members }}
- {{ program.members == 1 ? __('member') : __('members') }}
-
-
- {{ program.progress }}{{ __('% completed') }}
-
-
-
-
-
-
-
-
-
- {{ __('No courses in this program') }}
-
-
-
-
-
-
-
-
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 @@
+
+
+
+
+
+
+
+
+ {{ program.name }}
+
+
+
+
+ {{ program.course_count }}
+ {{ program.course_count == 1 ? __('Course') : __('Courses') }}
+
+
+
+
+
+ {{ program.member_count || 0 }}
+ {{ program.member_count == 1 ? __('member') : __('members') }}
+
+
+
+
+
+
+
+
+
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(