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..2f99bbed 100644
--- a/frontend/src/components/AppSidebar.vue
+++ b/frontend/src/components/AppSidebar.vue
@@ -196,7 +196,7 @@ import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar'
import { useSettings } from '@/stores/settings'
-import { Button, createResource, Tooltip } from 'frappe-ui'
+import { Button, call, createResource, Tooltip } from 'frappe-ui'
import PageModal from '@/components/Modals/PageModal.vue'
import { capture } from '@/telemetry'
import LMSLogo from '@/components/Icons/LMSLogo.vue'
@@ -214,6 +214,7 @@ import {
Users,
BookText,
Zap,
+ Check,
} from 'lucide-vue-next'
import {
TrialBanner,
@@ -360,35 +361,26 @@ const addProgrammingExercises = () => {
}
}
-const addPrograms = () => {
- let activeFor = ['Programs', 'ProgramForm']
+const addPrograms = async () => {
+ let canAddProgram = await checkIfCanAddProgram()
+ if (!canAddProgram) return
+ let activeFor = ['Programs', 'ProgramDetail']
let index = 1
- let canAddProgram = false
- if (
- !isInstructor.value &&
- !isModerator.value &&
- settingsStore.learningPaths.data
- ) {
- sidebarLinks.value = sidebarLinks.value.filter(
- (link) => link.label !== 'Courses'
- )
- activeFor.push('CourseDetail')
- activeFor.push('Lesson')
- index = 0
- canAddProgram = true
- } else if (isInstructor.value || isModerator.value) {
- canAddProgram = true
- }
+ sidebarLinks.value.splice(index, 0, {
+ label: 'Programs',
+ icon: 'Route',
+ to: 'Programs',
+ activeFor: activeFor,
+ })
+}
- if (canAddProgram) {
- sidebarLinks.value.splice(index, 0, {
- label: 'Programs',
- icon: 'Route',
- to: 'Programs',
- activeFor: activeFor,
- })
+const checkIfCanAddProgram = async () => {
+ if (isModerator.value || isInstructor.value) {
+ return true
}
+ const programs = await call('lms.lms.utils.get_programs')
+ return programs.enrolled.length > 0 || programs.published.length > 0
}
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/MobileLayout.vue b/frontend/src/components/MobileLayout.vue
index 40cb3855..7fb29b87 100644
--- a/frontend/src/components/MobileLayout.vue
+++ b/frontend/src/components/MobileLayout.vue
@@ -56,6 +56,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/ProgramDetail.vue b/frontend/src/pages/Programs/ProgramDetail.vue
new file mode 100644
index 00000000..281f02f3
--- /dev/null
+++ b/frontend/src/pages/Programs/ProgramDetail.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+ {{ program.data.name }}
+
+
+
+ {{ program.data.progress }}% {{ __('completed') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Please complete the previous course to unlock this one.') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Programs/ProgramEnrollment.vue b/frontend/src/pages/Programs/ProgramEnrollment.vue
new file mode 100644
index 00000000..b82ba8c6
--- /dev/null
+++ b/frontend/src/pages/Programs/ProgramEnrollment.vue
@@ -0,0 +1,157 @@
+
+
+
+
diff --git a/frontend/src/pages/Programs/ProgramForm.vue b/frontend/src/pages/Programs/ProgramForm.vue
new file mode 100644
index 00000000..43119952
--- /dev/null
+++ b/frontend/src/pages/Programs/ProgramForm.vue
@@ -0,0 +1,586 @@
+
+
+
+
diff --git a/frontend/src/pages/Programs/ProgramProgressSummary.vue b/frontend/src/pages/Programs/ProgramProgressSummary.vue
new file mode 100644
index 00000000..624a3390
--- /dev/null
+++ b/frontend/src/pages/Programs/ProgramProgressSummary.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('No members found.') }}
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Programs/Programs.vue b/frontend/src/pages/Programs/Programs.vue
new file mode 100644
index 00000000..20882844
--- /dev/null
+++ b/frontend/src/pages/Programs/Programs.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+ {{ __('{0} Programs').format(programs.data.length) }}
+
+
+
+
+ {{ 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/StudentPrograms.vue b/frontend/src/pages/Programs/StudentPrograms.vue
new file mode 100644
index 00000000..8baeb0e4
--- /dev/null
+++ b/frontend/src/pages/Programs/StudentPrograms.vue
@@ -0,0 +1,105 @@
+
+
+
+
+ {{ __('All Programs') }}
+
+
+
+
+
+
+
+
+ {{ program.name }}
+
+
+
+
+
+
+ {{ program.course_count }}
+ {{ program.course_count == 1 ? __('course') : __('courses') }}
+
+
+
+
+
+ {{ program.member_count || 0 }}
+ {{ program.member_count == 1 ? __('member') : __('members') }}
+
+
+
+
+
+
+
+ {{ Math.ceil(program.progress) }}% {{ __('completed') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/Programs/types.ts b/frontend/src/pages/Programs/types.ts
new file mode 100644
index 00000000..82d75bc9
--- /dev/null
+++ b/frontend/src/pages/Programs/types.ts
@@ -0,0 +1,50 @@
+interface Program {
+ name: string;
+ title: string;
+ published: boolean;
+ enforce_course_order: 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..a25d12f6 100644
--- a/frontend/src/router.js
+++ b/frontend/src/router.js
@@ -183,16 +183,16 @@ 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: '/programs/:programName',
+ name: 'ProgramDetail',
+ component: () => import('@/pages/Programs/ProgramDetail.vue'),
+ props: true,
},
{
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/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_course_progress/lms_course_progress.py b/lms/lms/doctype/lms_course_progress/lms_course_progress.py
index e03f662c..3b9fa05a 100644
--- a/lms/lms/doctype/lms_course_progress/lms_course_progress.py
+++ b/lms/lms/doctype/lms_course_progress/lms_course_progress.py
@@ -4,6 +4,7 @@
import frappe
from frappe.model.document import Document
+from lms.lms.doctype.lms_enrollment.lms_enrollment import update_program_progress
from lms.lms.utils import get_course_progress
@@ -19,3 +20,4 @@ class LMSCourseProgress(Document):
"name",
)
frappe.db.set_value("LMS Enrollment", membership, "progress", progress)
+ update_program_progress(self.member)
diff --git a/lms/lms/doctype/lms_enrollment/lms_enrollment.py b/lms/lms/doctype/lms_enrollment/lms_enrollment.py
index af13f3cd..14640c52 100644
--- a/lms/lms/doctype/lms_enrollment/lms_enrollment.py
+++ b/lms/lms/doctype/lms_enrollment/lms_enrollment.py
@@ -13,7 +13,7 @@ class LMSEnrollment(Document):
self.validate_membership_in_different_batch_same_course()
def on_update(self):
- self.update_program_progress()
+ update_program_progress(self.member)
def validate_membership_in_same_batch(self):
filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]}
@@ -59,21 +59,20 @@ class LMSEnrollment(Document):
)
)
- def update_program_progress(self):
- programs = frappe.get_all("LMS Program Member", {"member": self.member}, ["parent", "name"])
- for program in programs:
- total_progress = 0
- courses = frappe.get_all("LMS Program Course", {"parent": program.parent}, pluck="course")
- for course in courses:
- progress = frappe.db.get_value(
- "LMS Enrollment", {"course": course, "member": self.member}, "progress"
- )
- progress = progress or 0
- total_progress += progress
+def update_program_progress(member):
+ programs = frappe.get_all("LMS Program Member", {"member": member}, ["parent", "name"])
- average_progress = ceil(total_progress / len(courses))
- frappe.db.set_value("LMS Program Member", program.name, "progress", average_progress)
+ for program in programs:
+ total_progress = 0
+ courses = frappe.get_all("LMS Program Course", {"parent": program.parent}, pluck="course")
+ for course in courses:
+ progress = frappe.db.get_value("LMS Enrollment", {"course": course, "member": member}, "progress")
+ progress = progress or 0
+ total_progress += progress
+
+ average_progress = ceil(total_progress / len(courses))
+ frappe.db.set_value("LMS Program Member", program.name, "progress", average_progress)
@frappe.whitelist()
diff --git a/lms/lms/doctype/lms_payment/lms_payment.json b/lms/lms/doctype/lms_payment/lms_payment.json
index d66f0183..aaea6a9a 100644
--- a/lms/lms/doctype/lms_payment/lms_payment.json
+++ b/lms/lms/doctype/lms_payment/lms_payment.json
@@ -161,8 +161,8 @@
"link_fieldname": "payment"
}
],
- "modified": "2025-03-13 15:31:38.019002",
- "modified_by": "Administrator",
+ "modified": "2025-08-19 10:33:15.457678",
+ "modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Payment",
"owner": "Administrator",
@@ -180,9 +180,11 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
- "title_field": "billing_name"
-}
\ No newline at end of file
+ "title_field": "billing_name",
+ "track_changes": 1
+}
diff --git a/lms/lms/doctype/lms_program/lms_program.json b/lms/lms/doctype/lms_program/lms_program.json
index f92c689a..09b19248 100644
--- a/lms/lms/doctype/lms_program/lms_program.json
+++ b/lms/lms/doctype/lms_program/lms_program.json
@@ -7,8 +7,17 @@
"engine": "InnoDB",
"field_order": [
"title",
+ "published",
+ "column_break_cwjx",
+ "enforce_course_order",
+ "column_break_mikl",
+ "section_break_vhhu",
"program_courses",
- "program_members"
+ "program_members",
+ "section_break_pppe",
+ "course_count",
+ "column_break_qwhf",
+ "member_count"
],
"fields": [
{
@@ -30,12 +39,61 @@
"label": "Title",
"reqd": 1,
"unique": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "published",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Published"
+ },
+ {
+ "default": "0",
+ "fieldname": "enforce_course_order",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Enforce Course Order"
+ },
+ {
+ "fieldname": "section_break_vhhu",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_cwjx",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_pppe",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "course_count",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Course Count"
+ },
+ {
+ "fieldname": "column_break_qwhf",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "member_count",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "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-20 12:28:57.238902",
+ "modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Program",
"naming_rule": "By fieldname",
@@ -76,10 +134,21 @@
"role": "Course Creator",
"share": 1,
"write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "LMS Student",
+ "share": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
+ "title_field": "title",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/lms/lms/doctype/lms_program/lms_program.py b/lms/lms/doctype/lms_program/lms_program.py
index d0d6fa13..95bd312b 100644
--- a/lms/lms/doctype/lms_program/lms_program.py
+++ b/lms/lms/doctype/lms_program/lms_program.py
@@ -10,6 +10,7 @@ class LMSProgram(Document):
def validate(self):
self.validate_program_courses()
self.validate_program_members()
+ self.update_count()
def validate_program_courses(self):
courses = [row.course for row in self.program_courses]
@@ -30,3 +31,13 @@ class LMSProgram(Document):
frappe.bold(next(iter(duplicates)))
)
)
+
+ def update_count(self):
+ course_count = len(self.program_courses)
+ member_count = len(self.program_members)
+
+ if self.course_count != course_count:
+ self.course_count = course_count
+
+ if self.member_count != member_count:
+ self.member_count = member_count
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..3e1f489b 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -1118,6 +1118,7 @@ def get_course_details(course):
"lessons",
"enrollments",
"rating",
+ "card_gradient",
],
as_dict=1,
)
@@ -1270,7 +1271,6 @@ def get_lesson(course, chapter, lesson):
progress = get_progress(course, lesson_details.name)
lesson_details.chapter_title = frappe.db.get_value("Course Chapter", chapter_name, "title")
- lesson_details.rendered_content = render_html(lesson_details)
neighbours = get_neighbour_lesson(course, chapter, lesson)
lesson_details.next = neighbours["next"]
lesson_details.progress = progress
@@ -1919,80 +1919,93 @@ def update_certificate_purchase(course, payment_name):
@frappe.whitelist()
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"]
+ enrolled_programs = frappe.get_all(
+ "LMS Program Member", {"member": frappe.session.user}, ["parent as name", "progress"]
+ )
+ for program in enrolled_programs:
+ program.update(
+ frappe.db.get_value(
+ "LMS Program", program.name, ["name", "course_count", "member_count"], as_dict=True
+ )
)
- for program in programs:
- program_courses = frappe.get_all(
- "LMS Program Course", {"parent": program.name}, ["course"], order_by="idx"
- )
- program.courses = []
- previous_progress = 0
- for i, course in enumerate(program_courses):
- details = get_course_details(course.course)
- if i == 0:
- details.eligible = True
- elif previous_progress == 100:
- details.eligible = True
- else:
- details.eligible = False
+ published_programs = frappe.get_all(
+ "LMS Program",
+ {
+ "published": 1,
+ },
+ ["name", "course_count", "member_count"],
+ )
- previous_progress = details.membership.progress if details.membership else 0
- program.courses.append(details)
+ programs_to_remove = []
+ for program in published_programs:
+ if program.name in [p.name for p in enrolled_programs]:
+ programs_to_remove.append(program)
+ published_programs = [program for program in published_programs if program not in programs_to_remove]
- program.members = frappe.db.count("LMS Program Member", {"parent": program.name})
-
- return programs
+ return {
+ "enrolled": enrolled_programs,
+ "published": published_programs,
+ }
@frappe.whitelist()
-def enroll_in_program_course(program, course):
- enrollment = frappe.db.exists("LMS Enrollment", {"member": frappe.session.user, "course": course})
-
- if enrollment:
- enrollment = frappe.db.get_value("LMS Enrollment", enrollment, ["name", "current_lesson"], as_dict=1)
- enrollment.current_lesson = get_lesson_index(enrollment.current_lesson)
- return enrollment
-
+def get_program_details(program_name):
+ program = frappe.db.get_value(
+ "LMS Program",
+ program_name,
+ [
+ "name",
+ "member_count",
+ "course_count",
+ "published",
+ "allow_self_enrollment",
+ "enforce_course_order",
+ ],
+ as_dict=1,
+ )
program_courses = frappe.get_all(
- "LMS Program Course", {"parent": program}, ["course", "idx"], order_by="idx"
+ "LMS Program Course", {"parent": program_name}, ["course"], order_by="idx"
)
- current_course_idx = [
- program_course.idx for program_course in program_courses if program_course.course == course
- ][0]
- for program_course in program_courses:
- if program_course.idx < current_course_idx:
- enrollment = frappe.db.get_value(
- "LMS Enrollment",
- {"member": frappe.session.user, "course": program_course.course},
- ["name", "progress"],
- as_dict=1,
+ program.courses = []
+ previous_progress = 0
+ for i, course in enumerate(program_courses):
+ details = get_course_details(course.course)
+ if i == 0:
+ details.eligible = True
+ elif previous_progress == 100:
+ details.eligible = True
+ else:
+ details.eligible = False
+
+ previous_progress = details.membership.progress if details.membership else 0
+ program.courses.append(details)
+ if frappe.session.user != "Guest":
+ program.progress = frappe.db.get_value(
+ "LMS Program Member",
+ {"parent": program_name, "member": frappe.session.user},
+ "progress",
)
- if enrollment and enrollment.progress != 100:
- frappe.throw(
- _("Please complete the previous courses in the program to enroll in this course.")
- )
- elif not enrollment:
- frappe.throw(
- _("Please complete the previous courses in the program to enroll in this course.")
- )
- else:
- continue
- enrollment = frappe.new_doc("LMS Enrollment")
- enrollment.update(
- {
- "member": frappe.session.user,
- "course": course,
- }
- )
- enrollment.save()
- return enrollment
+ return program
+
+
+@frappe.whitelist()
+def enroll_in_program(program):
+ if frappe.session.user == "Guest":
+ frappe.throw(_("Please login to enroll in the program."))
+ if not frappe.db.exists("LMS Program Member", {"parent": program, "member": frappe.session.user}):
+ program_member = frappe.new_doc("LMS Program Member")
+ program_member.update(
+ {
+ "parent": program,
+ "parenttype": "LMS Program",
+ "parentfield": "members",
+ "member": frappe.session.user,
+ }
+ )
+ program_member.save(ignore_permissions=True)
@frappe.whitelist(allow_guest=True)
diff --git a/lms/patches.txt b/lms/patches.txt
index efc2345c..79b4afad 100644
--- a/lms/patches.txt
+++ b/lms/patches.txt
@@ -109,4 +109,5 @@ lms.patches.v2_0.link_zoom_account_to_live_class
lms.patches.v2_0.link_zoom_account_to_batch
lms.patches.v2_0.sidebar_for_certified_members
lms.patches.v2_0.move_batch_instructors_to_evaluators
-lms.patches.v2_0.enable_programming_exercises_in_sidebar
\ No newline at end of file
+lms.patches.v2_0.enable_programming_exercises_in_sidebar
+lms.patches.v2_0.count_in_program
\ No newline at end of file
diff --git a/lms/patches/v2_0/count_in_program.py b/lms/patches/v2_0/count_in_program.py
new file mode 100644
index 00000000..fb263a91
--- /dev/null
+++ b/lms/patches/v2_0/count_in_program.py
@@ -0,0 +1,18 @@
+import frappe
+
+
+def execute():
+ programs = frappe.get_all("LMS Program", pluck="name")
+
+ for program in programs:
+ course_count = frappe.db.count(
+ "LMS Program Course",
+ {"parent": program, "parenttype": "LMS Program", "parentfield": "program_courses"},
+ )
+ frappe.db.set_value("LMS Program", program, "course_count", course_count)
+
+ member_count = frappe.db.count(
+ "LMS Program Member",
+ {"parent": program, "parenttype": "LMS Program", "parentfield": "program_members"},
+ )
+ frappe.db.set_value("LMS Program", program, "member_count", member_count)