Merge pull request #1686 from pateljannat/program-refactor

refactor: learning path
This commit is contained in:
Jannat Patel
2025-08-20 14:09:41 +05:30
committed by GitHub
34 changed files with 1590 additions and 776 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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"
}
"title_field": "billing_name",
"track_changes": 1
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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": []
}
}

View File

@@ -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": []
}
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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
lms.patches.v2_0.enable_programming_exercises_in_sidebar
lms.patches.v2_0.count_in_program

View File

@@ -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)