From 64ed0b3e948e90a3d2c550f9ed69f7c9dedce194 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 21 Nov 2024 17:10:24 +0530 Subject: [PATCH] feat: program restrictions --- frontend/src/components/AppSidebar.vue | 19 ++++- frontend/src/components/CourseOutline.vue | 2 +- frontend/src/components/Quiz.vue | 3 + frontend/src/pages/Courses.vue | 16 ++-- frontend/src/pages/ProgramForm.vue | 3 - frontend/src/pages/Programs.vue | 85 ++++++++++++------- frontend/src/stores/settings.js | 13 +++ frontend/src/utils/index.js | 2 - .../doctype/course_lesson/course_lesson.py | 8 +- .../doctype/lms_enrollment/lms_enrollment.py | 24 ++++++ lms/lms/doctype/lms_program/lms_program.py | 76 ----------------- .../lms_program_member.json | 12 ++- lms/lms/utils.py | 56 +++++++++++- lms/lms/workspace/lms/lms.json | 8 +- 14 files changed, 190 insertions(+), 137 deletions(-) diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 2c0afea4..7dba321c 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -99,6 +99,7 @@ import { getSidebarLinks } from '../utils' import { usersStore } from '@/stores/user' import { sessionStore } from '@/stores/session' import { useSidebar } from '@/stores/sidebar' +import { useSettings } from '@/stores/settings' import { ChevronRight, Plus } from 'lucide-vue-next' import { createResource, Button } from 'frappe-ui' import PageModal from '@/components/Modals/PageModal.vue' @@ -114,6 +115,7 @@ const isModerator = ref(false) const isInstructor = ref(false) const pageToEdit = ref(null) const showWebPages = ref(false) +const settingsStore = useSettings() onMounted(() => { socket.on('publish_lms_notifications', (data) => { @@ -184,12 +186,23 @@ const addQuizzes = () => { } const addPrograms = () => { - if (isInstructor.value || isModerator.value) { - sidebarLinks.value.push({ + if (settingsStore.learningPaths.data) { + let activeFor = ['Programs', 'ProgramForm'] + let index = 1 + if (!isInstructor.value && !isModerator.value) { + sidebarLinks.value = sidebarLinks.value.filter( + (link) => link.label !== 'Courses' + ) + activeFor.push('CourseDetail') + activeFor.push('Lesson') + index = 0 + } + + sidebarLinks.value.splice(index, 0, { label: 'Programs', icon: 'Route', to: 'Programs', - activeFor: ['Programs', 'ProgramForm'], + activeFor: activeFor, }) } } diff --git a/frontend/src/components/CourseOutline.vue b/frontend/src/components/CourseOutline.vue index d93dbc2d..04f73937 100644 --- a/frontend/src/components/CourseOutline.vue +++ b/frontend/src/components/CourseOutline.vue @@ -303,9 +303,9 @@ const trashChapter = (chapterName) => { } const redirectToChapter = (chapter) => { + if (!chapter.is_scorm_package) return event.preventDefault() if (props.allowEdit) return - if (!chapter.is_scorm_package) return if (!user.data) { showToast( __('You are not enrolled'), diff --git a/frontend/src/components/Quiz.vue b/frontend/src/components/Quiz.vue index 79096e9e..b4b3c42f 100644 --- a/frontend/src/components/Quiz.vue +++ b/frontend/src/components/Quiz.vue @@ -397,6 +397,9 @@ const attempts = createResource({ watch( () => quiz.data, () => { + if (quiz.data) { + populateQuestions() + } if (quiz.data && quiz.data.max_attempts) { attempts.reload() resetQuiz() diff --git a/frontend/src/pages/Courses.vue b/frontend/src/pages/Courses.vue index 8fc7b1e8..16ae3ba0 100644 --- a/frontend/src/pages/Courses.vue +++ b/frontend/src/pages/Courses.vue @@ -173,12 +173,14 @@ import { BookOpen, Plus, Search } from 'lucide-vue-next' import { ref, computed, inject, onMounted, watch } from 'vue' import { updateDocumentTitle } from '@/utils' import { useRouter } from 'vue-router' +import { useSettings } from '@/stores/settings' const user = inject('$user') const searchQuery = ref('') const currentCategory = ref(null) const hasCourses = ref(false) const router = useRouter() +const settings = useSettings() onMounted(() => { checkLearningPath() @@ -189,14 +191,12 @@ onMounted(() => { }) const checkLearningPath = () => { - call('frappe.client.get_single_value', { - doctype: 'LMS Settings', - field: 'enable_learning_paths', - }).then((res) => { - if (res && !user.data?.is_moderator && !user.data?.is_instructor) { - router.push({ name: 'Programs' }) - } - }) + if ( + settings.learningPaths.data && + (!user.data?.is_moderator || !user.data?.is_instructor) + ) { + router.push({ name: 'Programs' }) + } } const courses = createResource({ diff --git a/frontend/src/pages/ProgramForm.vue b/frontend/src/pages/ProgramForm.vue index 13f15d3c..092e41b8 100644 --- a/frontend/src/pages/ProgramForm.vue +++ b/frontend/src/pages/ProgramForm.vue @@ -211,8 +211,6 @@ const program = createDocumentResource({ cache: ['program', props.programName], }) -console.log(program) - const addProgramCourse = () => { program.setValue.submit( { @@ -288,7 +286,6 @@ const updateOrder = (e) => { course.idx = index + 1 }) - console.log(courses) program.setValue.submit( { program_courses: courses, diff --git a/frontend/src/pages/Programs.vue b/frontend/src/pages/Programs.vue index 29f5c09b..860a1244 100644 --- a/frontend/src/pages/Programs.vue +++ b/frontend/src/pages/Programs.vue @@ -15,15 +15,27 @@
-
+
{{ program.name }}
- - {{ program.members }} {{ __('Members') }} + + {{ program.members }} + {{ + program.members == 1 ? __(singularize('members')) : __('members') + }} + + {{ program.progress }}{{ __('% completed') }} + + - - - + :course="course" + @click="enrollMember(program.name, course.name)" + class="cursor-pointer" + />
{{ __('No courses in this program') }} @@ -128,6 +117,7 @@ import { computed, inject, ref } from 'vue' import { BookOpen, Edit, Plus } from 'lucide-vue-next' import CourseCard from '@/components/CourseCard.vue' import { useRouter } from 'vue-router' +import { showToast, singularize } from '@/utils' const user = inject('$user') const showDialog = ref(false) @@ -140,8 +130,6 @@ const programs = createResource({ cache: 'programs', }) -console.log(programs) - const createProgram = (close) => { call('frappe.client.insert', { doc: { @@ -153,6 +141,37 @@ const createProgram = (close) => { }) } +const enrollMember = (program, course) => { + call('lms.lms.utils.enroll_in_program_course', { + program: program, + course: course, + }) + .then((data) => { + if (data.current_lesson) { + router.push({ + name: 'Lesson', + params: { + courseName: course, + chapterNumber: data.current_lesson.split('-')[0], + lessonNumber: data.current_lesson.split('-')[1], + }, + }) + } else if (data) { + router.push({ + name: 'Lesson', + params: { + courseName: course, + chapterNumber: 1, + lessonNumber: 1, + }, + }) + } + }) + .catch((err) => { + showToast('Error', err.messages?.[0] || err, 'x') + }) +} + const breadbrumbs = computed(() => [ { label: 'Programs', diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js index 61d6bedc..9844517f 100644 --- a/frontend/src/stores/settings.js +++ b/frontend/src/stores/settings.js @@ -1,12 +1,25 @@ import { defineStore } from 'pinia' import { ref } from 'vue' +import { createResource } from 'frappe-ui' export const useSettings = defineStore('settings', () => { const isSettingsOpen = ref(false) const activeTab = ref(null) + const learningPaths = createResource({ + url: 'frappe.client.get_single_value', + makeParams(values) { + return { + doctype: 'LMS Settings', + field: 'enable_learning_paths', + } + }, + auto: true, + cache: ['learningPaths'], + }) return { isSettingsOpen, activeTab, + learningPaths, } }) diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index 1f3eaa11..aaf79771 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -438,8 +438,6 @@ export function getSidebarLinks() { 'Lesson', 'CourseForm', 'LessonForm', - 'Programs', - 'ProgramForm', ], }, { diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index e1aac53c..2955305b 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -93,15 +93,15 @@ def save_progress(lesson, course): frappe.db.set_value("LMS Enrollment", membership, "current_lesson", lesson) - quiz_completed = get_quiz_progress(lesson) - if not quiz_completed: - return 0 - if frappe.db.exists( "LMS Course Progress", {"lesson": lesson, "member": frappe.session.user} ): return + quiz_completed = get_quiz_progress(lesson) + if not quiz_completed: + return 0 + frappe.get_doc( { "doctype": "LMS Course Progress", diff --git a/lms/lms/doctype/lms_enrollment/lms_enrollment.py b/lms/lms/doctype/lms_enrollment/lms_enrollment.py index 04f51875..389e1b2d 100644 --- a/lms/lms/doctype/lms_enrollment/lms_enrollment.py +++ b/lms/lms/doctype/lms_enrollment/lms_enrollment.py @@ -4,6 +4,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils import ceil class LMSEnrollment(Document): @@ -11,6 +12,9 @@ class LMSEnrollment(Document): self.validate_membership_in_same_batch() self.validate_membership_in_different_batch_same_course() + def on_update(self): + self.update_program_progress() + def validate_membership_in_same_batch(self): filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]} if self.batch_old: @@ -55,6 +59,26 @@ 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 + + average_progress = ceil(total_progress / len(courses)) + frappe.db.set_value("LMS Program Member", program.name, "progress", average_progress) + @frappe.whitelist() def create_membership( diff --git a/lms/lms/doctype/lms_program/lms_program.py b/lms/lms/doctype/lms_program/lms_program.py index 0de0f5a7..d0d6fa13 100644 --- a/lms/lms/doctype/lms_program/lms_program.py +++ b/lms/lms/doctype/lms_program/lms_program.py @@ -11,82 +11,6 @@ class LMSProgram(Document): self.validate_program_courses() self.validate_program_members() - def on_update(self): - self.manage_acccess() - - def manage_acccess(self): - old_doc = self.get_doc_before_save() - - if not old_doc: - return - - previous_courses = [row.course for row in old_doc.program_courses] - current_courses = [row.course for row in self.program_courses] - - print("previous_courses", previous_courses) - print("current_courses", current_courses) - - previous_members = [row.member for row in old_doc.program_members] - current_members = [row.member for row in self.program_members] - - print("previous_members", previous_members) - print("current_members", current_members) - - courses_added = [ - course for course in current_courses if course not in previous_courses - ] - courses_removed = [ - course for course in previous_courses if course not in current_courses - ] - - members_added = [ - member for member in current_members if member not in previous_members - ] - members_removed = [ - member for member in previous_members if member not in current_members - ] - - print(courses_removed) - print(members_removed) - - if len(courses_added) > 0: - self.grant_program_access(current_members, courses_added) - - if len(courses_removed) > 0: - print(courses_removed) - self.revoke_program_access(current_members, courses_removed) - - if len(members_added) > 0: - self.grant_program_access(members_added, current_courses) - - if len(members_removed) > 0: - print(members_removed) - self.revoke_program_access(members_removed, current_courses) - - def grant_program_access(self, members, courses): - for course in courses: - for member in members: - enrollment = frappe.db.exists( - "LMS Enrollment", {"course": course, "member": member} - ) - if not enrollment: - enrollment = frappe.new_doc("LMS Enrollment") - enrollment.course = course - enrollment.member = member - enrollment.insert() - - def revoke_program_access(self, members, courses): - for course in courses: - print(course) - for member in members: - print(member) - enrollment = frappe.db.exists( - "LMS Enrollment", {"course": course, "member": member} - ) - print(enrollment) - if enrollment: - frappe.delete_doc("LMS Enrollment", enrollment) - def validate_program_courses(self): courses = [row.course for row in self.program_courses] duplicates = {course for course in courses if courses.count(course) > 1} 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 d8553936..f629e1f2 100644 --- a/lms/lms/doctype/lms_program_member/lms_program_member.json +++ b/lms/lms/doctype/lms_program_member/lms_program_member.json @@ -7,7 +7,8 @@ "engine": "InnoDB", "field_order": [ "member", - "full_name" + "full_name", + "progress" ], "fields": [ { @@ -25,12 +26,19 @@ "in_list_view": 1, "label": "Full Name", "read_only": 1 + }, + { + "default": "0", + "fieldname": "progress", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Progress" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-11-18 12:44:02.648786", + "modified": "2024-11-21 12:51:31.882576", "modified_by": "Administrator", "module": "LMS", "name": "LMS Program Member", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index b94c0350..3c0ba3c0 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -1763,12 +1763,12 @@ def get_programs(): programs = frappe.get_all("LMS Program", fields=["name"]) else: programs = frappe.get_all( - "LMS Program Member", {"member": frappe.session.user}, ["parent as name"] + "LMS Program Member", {"member": frappe.session.user}, ["parent as name", "progress"] ) for program in programs: program_courses = frappe.get_all( - "LMS Program Course", {"parent": program.name}, ["course"] + "LMS Program Course", {"parent": program.name}, ["course"], order_by="idx" ) program.courses = [] for course in program_courses: @@ -1777,3 +1777,55 @@ def get_programs(): program.members = frappe.db.count("LMS Program Member", {"parent": program.name}) return 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 + + program_courses = frappe.get_all( + "LMS Program Course", {"parent": program}, ["course", "idx"], 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, + ) + 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 diff --git a/lms/lms/workspace/lms/lms.json b/lms/lms/workspace/lms/lms.json index 2a52d245..f92a845e 100644 --- a/lms/lms/workspace/lms/lms.json +++ b/lms/lms/workspace/lms/lms.json @@ -1,4 +1,5 @@ { + "app": "lms", "charts": [ { "chart_name": "New Signups", @@ -145,7 +146,7 @@ "type": "Link" } ], - "modified": "2024-08-09 13:19:06.273056", + "modified": "2024-11-21 12:16:25.886431", "modified_by": "Administrator", "module": "LMS", "name": "LMS", @@ -212,5 +213,6 @@ "type": "DocType" } ], - "title": "LMS" -} + "title": "LMS", + "type": "Workspace" +} \ No newline at end of file