@@ -110,6 +115,7 @@
import {
Avatar,
Button,
+ createListResource,
createResource,
FeatherIcon,
ListHeader,
@@ -139,39 +145,47 @@ const props = defineProps({
},
})
-const students = createResource({
- url: 'lms.lms.utils.get_batch_students',
+const studentCount = createResource({
+ url: 'lms.lms.utils.get_batch_student_count',
+ cache: ['batch_student_count', props.batch?.data?.name],
params: {
batch: props.batch?.data?.name,
},
auto: true,
})
-const getStudentColumns = () => {
- let columns = [
- {
- label: 'Full Name',
- key: 'full_name',
- width: '20rem',
- icon: 'user',
- },
- {
- label: 'Progress',
- key: 'progress',
- width: '15rem',
- icon: 'activity',
- },
- {
- label: 'Last Active',
- key: 'last_active',
- width: '10rem',
- align: 'center',
- icon: 'clock',
- },
- ]
+const students = createListResource({
+ doctype: 'LMS Batch Enrollment',
+ url: 'lms.lms.utils.get_batch_students',
+ cache: ['batch_students', props.batch?.data?.name],
+ pageLength: 50,
+ filters: {
+ batch: props.batch?.data?.name,
+ },
+ auto: true,
+})
- return columns
-}
+const studentColumns = [
+ {
+ label: 'Full Name',
+ key: 'full_name',
+ width: '20rem',
+ icon: 'user',
+ },
+ {
+ label: 'Progress',
+ key: 'progress',
+ width: '15rem',
+ icon: 'activity',
+ },
+ {
+ label: 'Last Active',
+ key: 'last_active',
+ width: '10rem',
+ align: 'center',
+ icon: 'clock',
+ },
+]
const openStudentModal = () => {
showStudentModal.value = true
@@ -200,6 +214,7 @@ const removeStudents = (selections, unselectAll) => {
{
onSuccess(data) {
students.reload()
+ studentCount.reload()
props.batch.reload()
toast.success(__('Students deleted successfully'))
unselectAll()
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index 9bad9eee..998e79c0 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -1354,17 +1354,30 @@ def get_exercise_details(assessment, member):
@frappe.whitelist()
-def get_batch_students(batch):
+def get_batch_students(filters, offset=0, limit_start=0, limit_page_length=None, limit=None):
+ # limit_start and limit_page_length are used for backward compatibility
+ start = limit_start or offset
+ page_length = limit_page_length or limit
+
+ batch = filters.get("batch")
+ if not batch:
+ return []
+
students = []
students_list = frappe.get_all(
- "LMS Batch Enrollment", filters={"batch": batch}, fields=["member", "name"]
+ "LMS Batch Enrollment",
+ filters={"batch": batch},
+ fields=["member", "name"],
+ offset=start,
+ limit=page_length,
+ order_by="creation desc",
)
for student in students_list:
details = get_batch_student_details(student)
calculate_student_progress(batch, details)
students.append(details)
- students = sorted(students, key=lambda x: x.progress, reverse=True)
+
return students
From c59be28a265558d12fd9611e233c0e30a28d08bd Mon Sep 17 00:00:00 2001
From: raizasafeel <89463672+raizasafeel@users.noreply.github.com>
Date: Wed, 14 Jan 2026 14:08:04 +0530
Subject: [PATCH 2/3] perf(batch): optimise dashboard with query builder
---
.../src/components/AdminBatchDashboard.vue | 104 +++++----------
lms/lms/utils.py | 119 +++++++++++++++++-
2 files changed, 146 insertions(+), 77 deletions(-)
diff --git a/frontend/src/components/AdminBatchDashboard.vue b/frontend/src/components/AdminBatchDashboard.vue
index 934ab995..947180c5 100644
--- a/frontend/src/components/AdminBatchDashboard.vue
+++ b/frontend/src/components/AdminBatchDashboard.vue
@@ -8,7 +8,7 @@
@@ -37,7 +37,7 @@
v-if="showProgressChart"
class="border"
:config="{
- data: chartData || [],
+ data: filteredChartData,
title: __('Batch Summary'),
subtitle: __('Progress of students in courses and assessments'),
xAxis: {
@@ -64,96 +64,50 @@
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index 998e79c0..0acb2565 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -24,6 +24,8 @@ from frappe.utils import (
pretty_date,
rounded,
)
+from pypika import Case
+from pypika import functions as fn
from lms.lms.md import find_macros
@@ -1381,6 +1383,116 @@ def get_batch_students(filters, offset=0, limit_start=0, limit_page_length=None,
return students
+@frappe.whitelist()
+def get_batch_student_count(batch):
+ if not frappe.db.exists("LMS Batch", batch):
+ frappe.throw(_("The specified batch does not exist."))
+ return frappe.db.count("LMS Batch Enrollment", filters={"batch": batch})
+
+
+@frappe.whitelist()
+def get_batch_certificate_count(batch):
+ if not frappe.db.exists("LMS Batch", batch):
+ frappe.throw(_("The specified batch does not exist."))
+ return frappe.db.count("LMS Certificate", filters={"batch_name": batch})
+
+
+@frappe.whitelist()
+def get_batch_assessment_count(batch):
+ if not frappe.db.exists("LMS Batch", batch):
+ frappe.throw(_("The specified batch does not exist."))
+ return frappe.db.count("LMS Assessment", filters={"parent": batch})
+
+
+def get_course_completion_stats(batch):
+ """Get completion counts per course in batch"""
+ BatchCourse = frappe.qb.DocType("Batch Course")
+ BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
+ Enrollment = frappe.qb.DocType("LMS Enrollment")
+
+ rows = (
+ frappe.qb.from_(BatchCourse)
+ .left_join(BatchEnrollment)
+ .on(BatchEnrollment.batch == BatchCourse.parent)
+ .left_join(Enrollment)
+ .on((Enrollment.course == BatchCourse.course) & (Enrollment.member == BatchEnrollment.member))
+ .where(BatchCourse.parent == batch)
+ .groupby(BatchCourse.course, BatchCourse.title)
+ .select(
+ BatchCourse.title,
+ fn.Count(Case().when(Enrollment.progress == 100, Enrollment.member)).distinct().as_("completed"),
+ )
+ ).run(as_dict=True)
+
+ return [{"task": row.title, "value": row.completed or 0} for row in rows]
+
+
+def get_assignment_pass_stats(batch):
+ """Get pass counts per assignment in batch"""
+ Assessment = frappe.qb.DocType("LMS Assessment")
+ Assignment = frappe.qb.DocType("LMS Assignment")
+ BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
+ Submission = frappe.qb.DocType("LMS Assignment Submission")
+
+ rows = (
+ frappe.qb.from_(Assessment)
+ .join(Assignment)
+ .on(Assignment.name == Assessment.assessment_name)
+ .left_join(BatchEnrollment)
+ .on(BatchEnrollment.batch == Assessment.parent)
+ .left_join(Submission)
+ .on(
+ (Submission.assignment == Assessment.assessment_name)
+ & (Submission.member == BatchEnrollment.member)
+ )
+ .where((Assessment.parent == batch) & (Assessment.assessment_type == "LMS Assignment"))
+ .groupby(Assessment.assessment_name, Assignment.title)
+ .select(
+ Assignment.title,
+ fn.Count(Case().when(Submission.status == "Pass", Submission.member)).distinct().as_("passed"),
+ )
+ ).run(as_dict=True)
+
+ return [{"task": row.title, "value": row.passed or 0} for row in rows]
+
+
+def get_quiz_pass_stats(batch):
+ """Get pass counts per quiz in batch"""
+ Assessment = frappe.qb.DocType("LMS Assessment")
+ Quiz = frappe.qb.DocType("LMS Quiz")
+ BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
+ Submission = frappe.qb.DocType("LMS Quiz Submission")
+
+ rows = (
+ frappe.qb.from_(Assessment)
+ .join(Quiz)
+ .on(Quiz.name == Assessment.assessment_name)
+ .left_join(BatchEnrollment)
+ .on(BatchEnrollment.batch == Assessment.parent)
+ .left_join(Submission)
+ .on((Submission.quiz == Assessment.assessment_name) & (Submission.member == BatchEnrollment.member))
+ .where((Assessment.parent == batch) & (Assessment.assessment_type == "LMS Quiz"))
+ .groupby(Assessment.assessment_name, Quiz.title)
+ .select(
+ Quiz.title,
+ fn.Count(Case().when(Submission.percentage >= Submission.passing_percentage, Submission.member))
+ .distinct()
+ .as_("passed"),
+ )
+ ).run(as_dict=True)
+
+ return [{"task": row.title, "value": row.passed or 0} for row in rows]
+
+
+@frappe.whitelist()
+def get_batch_chart_data(batch):
+ """Get completion counts per course and assessment"""
+ if not frappe.db.exists("LMS Batch", batch):
+ frappe.throw(_("The specified batch does not exist."))
+
+ return get_course_completion_stats(batch) + get_assignment_pass_stats(batch) + get_quiz_pass_stats(batch)
+
+
def get_batch_student_details(student):
details = frappe.db.get_value(
"User",
@@ -1423,8 +1535,11 @@ def calculate_course_progress(batch_courses, details):
details.courses = frappe._dict()
for course in batch_courses:
- progress = frappe.db.get_value(
- "LMS Enrollment", {"course": course.course, "member": details.email}, "progress"
+ progress = (
+ frappe.db.get_value(
+ "LMS Enrollment", {"course": course.course, "member": details.email}, "progress"
+ )
+ or 0
)
details.courses[course.title] = progress
course_progress.append(progress)
From e2c0355821a8302ee24e4ce5560487cbadc56627 Mon Sep 17 00:00:00 2001
From: raizasafeel <89463672+raizasafeel@users.noreply.github.com>
Date: Wed, 14 Jan 2026 23:25:11 +0530
Subject: [PATCH 3/3] refactor(batch): simplify dashboard with get_count and
conditional rendering
---
.../src/components/AdminBatchDashboard.vue | 7 ++++-
frontend/src/components/BatchStudents.vue | 9 +++---
lms/lms/utils.py | 28 +++++--------------
3 files changed, 18 insertions(+), 26 deletions(-)
diff --git a/frontend/src/components/AdminBatchDashboard.vue b/frontend/src/components/AdminBatchDashboard.vue
index 947180c5..135fe84c 100644
--- a/frontend/src/components/AdminBatchDashboard.vue
+++ b/frontend/src/components/AdminBatchDashboard.vue
@@ -82,18 +82,23 @@ const studentCount = createResource({
const assessmentCount = createResource({
url: 'lms.lms.utils.get_batch_assessment_count',
- params: { batch: props.batch?.data?.name },
+ cache: ['batch_assessment_count', props.batch?.data?.name],
+ params: {
+ batch: props.batch?.data?.name,
+ },
auto: true,
})
const chartData = createResource({
url: 'lms.lms.utils.get_batch_chart_data',
+ cache: ['batch_chart_data', props.batch?.data?.name],
params: { batch: props.batch?.data?.name },
auto: true,
})
const certificationCount = createResource({
url: 'frappe.client.get_count',
+ cache: ['batch_certificate_count', props.batch?.data?.name],
params: {
doctype: 'LMS Certificate',
filters: { batch_name: props.batch?.data?.name },
diff --git a/frontend/src/components/BatchStudents.vue b/frontend/src/components/BatchStudents.vue
index 26665db5..1eb56e17 100644
--- a/frontend/src/components/BatchStudents.vue
+++ b/frontend/src/components/BatchStudents.vue
@@ -88,8 +88,8 @@