diff --git a/frontend/src/components/CourseOutline.vue b/frontend/src/components/CourseOutline.vue index ce4a07dc..ba295734 100644 --- a/frontend/src/components/CourseOutline.vue +++ b/frontend/src/components/CourseOutline.vue @@ -112,6 +112,14 @@ v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 stroke-1 mr-2" /> + + - - - +
+ {{ dayjs(row[column.key]).format('DD MMM YYYY') }} +
+
+ {{ Math.ceil(row[column.key]) }}% +
+
+ {{ row[column.key].toString() }} +
+ + +
-
- {{ - Math.round((row.value / course.data?.enrollments) * 100) - }}% -
+ +
+ {{ + Math.round((row.value / course.data?.enrollments) * 100) + }}% +
+
+ diff --git a/frontend/src/pages/Home/StudentHome.vue b/frontend/src/pages/Home/StudentHome.vue index 40f66849..1c45e331 100644 --- a/frontend/src/pages/Home/StudentHome.vue +++ b/frontend/src/pages/Home/StudentHome.vue @@ -72,7 +72,7 @@ -
+
{{ diff --git a/lms/hooks.py b/lms/hooks.py index 26f1ce31..2a289ce1 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -3,7 +3,7 @@ import frappe from . import __version__ as app_version app_name = "frappe_lms" -app_title = "Frappe LMS" +app_title = "Learning" app_publisher = "Frappe" app_description = "Frappe LMS App" app_icon_url = "/assets/lms/images/lms-logo.png" diff --git a/lms/lms/api.py b/lms/lms/api.py index d7ac8173..9421853e 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -1654,8 +1654,12 @@ def get_progress_distribution(progressList): "value": len([p for p in progressList if 30 <= p < 60]), }, { - "name": "Advanced (60-100%)", - "value": len([p for p in progressList if 60 <= p <= 100]), + "name": "Advanced (60-99%)", + "value": len([p for p in progressList if 60 <= p < 100]), + }, + { + "name": "Completed (100%)", + "value": len([p for p in progressList if p == 100]), }, ] @@ -2048,13 +2052,17 @@ def get_lesson_completion_stats(course): Lesson = frappe.qb.DocType("Course Lesson") rows = ( - frappe.qb.from_(CourseProgress) - .join(LessonReference) - .on(CourseProgress.lesson == LessonReference.lesson) + frappe.qb.from_(LessonReference) .join(ChapterReference) .on(LessonReference.parent == ChapterReference.chapter) .join(Lesson) - .on(CourseProgress.lesson == Lesson.name) + .on(LessonReference.lesson == Lesson.name) + .left_join(CourseProgress) + .on( + (CourseProgress.lesson == LessonReference.lesson) + & (CourseProgress.course == course) + & (CourseProgress.status == "Complete") + ) .select( LessonReference.idx, ChapterReference.idx.as_("chapter_idx"), @@ -2063,10 +2071,132 @@ def get_lesson_completion_stats(course): Lesson.name.as_("lesson_name"), fn.Count(CourseProgress.name).as_("completion_count"), ) - .where((CourseProgress.course == course) & (CourseProgress.status == "Complete")) - .groupby(CourseProgress.lesson) + .where(ChapterReference.parent == course) + .groupby(LessonReference.lesson) .orderby(ChapterReference.idx, LessonReference.idx) .run(as_dict=True) ) return rows + + +@frappe.whitelist() +def get_course_assessment_progress(course, member): + if not can_modify_course(course): + frappe.throw( + _("You do not have permission to access this course's assessment data."), frappe.PermissionError + ) + + quizzes = get_course_quiz_progress(course, member) + assignments = get_course_assignment_progress(course, member) + programming_exercises = get_course_programming_exercise_progress(course, member) + + return { + "quizzes": quizzes, + "assignments": assignments, + "exercises": programming_exercises, + } + + +def get_course_quiz_progress(course, member): + quizzes = get_assessment_from_lesson(course, "quiz") + attempts = [] + + for quiz in quizzes: + submissions = frappe.get_all( + "LMS Quiz Submission", + { + "quiz": quiz, + "member": member, + }, + ["name", "score", "percentage", "quiz", "quiz_title"], + order_by="creation desc", + limit=1, + ) + if len(submissions): + attempts.append(submissions[0]) + else: + attempts.append( + { + "quiz": quiz, + "quiz_title": frappe.db.get_value("LMS Quiz", quiz, "title"), + "score": 0, + "percentage": 0, + } + ) + + return attempts + + +def get_course_assignment_progress(course, member): + assignments = get_assessment_from_lesson(course, "assignment") + submissions = [] + + for assignment in assignments: + assignment_subs = frappe.get_all( + "LMS Assignment Submission", + { + "assignment": assignment, + "member": member, + }, + ["name", "status", "assignment", "assignment_title"], + order_by="creation desc", + limit=1, + ) + if len(assignment_subs): + submissions.append(assignment_subs[0]) + else: + submissions.append( + { + "assignment": assignment, + "assignment_title": frappe.db.get_value("LMS Assignment", assignment, "title"), + "status": "Not Submitted", + } + ) + + return submissions + + +def get_course_programming_exercise_progress(course, member): + exercises = get_assessment_from_lesson(course, "program") + submissions = [] + + for exercise in exercises: + exercise_subs = frappe.get_all( + "LMS Programming Exercise Submission", + { + "exercise": exercise, + "member": member, + }, + ["name", "status", "exercise", "exercise_title"], + order_by="creation desc", + limit=1, + ) + if len(exercise_subs): + submissions.append(exercise_subs[0]) + else: + submissions.append( + { + "exercise": exercise, + "exercise_title": frappe.db.get_value("LMS Programming Exercise", exercise, "title"), + "status": "Not Attempted", + } + ) + + return submissions + + +def get_assessment_from_lesson(course, assessmentType): + assessments = [] + lessons = frappe.get_all("Course Lesson", {"course": course}, ["name", "title", "content"]) + + for lesson in lessons: + if lesson.content: + content = json.loads(lesson.content) + for block in content.get("blocks", []): + if block.get("type") == assessmentType: + data_field = "exercise" if assessmentType == "program" else assessmentType + quiz_name = block.get("data", {}).get(data_field) + assessments.append(quiz_name) + + return assessments diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 052efbdc..8c05ba82 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -205,6 +205,10 @@ def get_lesson_icon(body, content): if block.get("type") == "quiz": return "icon-quiz" + if block.get("type") == "assignment": + return "icon-assignment" + if block.get("type") == "program": + return "icon-code" return "icon-list"