mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
refactor: program list for students
This commit is contained in:
@@ -2,16 +2,9 @@
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Program'),
|
||||
size: '2xl',
|
||||
actions: [{
|
||||
label: __('Save'),
|
||||
variant: 'solid',
|
||||
onClick: ({ close }: { close: () => void }) => {
|
||||
saveProgram(close)
|
||||
}
|
||||
}]
|
||||
}"
|
||||
title: programName === 'new' ? __('Create Program') : __('Edit Program'),
|
||||
size: '2xl',
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="text-base">
|
||||
@@ -192,6 +185,25 @@
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="flex justify-end space-x-2 group">
|
||||
<Button
|
||||
v-if="programName != 'new'"
|
||||
@click="deleteProgram(close)"
|
||||
variant="outline"
|
||||
theme="red"
|
||||
class="invisible group-hover:visible"
|
||||
>
|
||||
<template #prefix>
|
||||
<Trash2 class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Delete') }}
|
||||
</Button>
|
||||
<Button variant="solid" @click="saveProgram(close)">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -499,6 +511,19 @@ const remove = async (
|
||||
unselectAll()
|
||||
}
|
||||
|
||||
const deleteProgram = (close: () => void) => {
|
||||
if (props.programName == 'new') return
|
||||
programs.value?.delete.submit(props.programName, {
|
||||
onSuccess() {
|
||||
toast.success(__('Program deleted successfully'))
|
||||
close()
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const courseColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -10,36 +10,38 @@
|
||||
{{ __('New') }}
|
||||
</Button>
|
||||
</header>
|
||||
<div
|
||||
v-if="programs.data?.length"
|
||||
class="grid grid-cols-3 gap-5 py-10 w-3/4 mx-auto"
|
||||
>
|
||||
<div
|
||||
v-for="program in programs.data"
|
||||
@click="openForm(program.name)"
|
||||
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer space-y-2"
|
||||
>
|
||||
<div class="text-lg font-semibold">
|
||||
{{ program.name }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
<span>
|
||||
{{ program.course_count }}
|
||||
{{ program.course_count == 1 ? __('Course') : __('Courses') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<User class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
<span>
|
||||
{{ program.member_count || 0 }}
|
||||
{{ program.member_count == 1 ? __('member') : __('members') }}
|
||||
</span>
|
||||
<div v-if="programs.data?.length && !isStudent" class="py-10 w-3/4 mx-auto">
|
||||
<div class="text-lg font-semibold text-ink-gray-9 mb-5">
|
||||
{{ __('{0} Programs').format(programs.data.length) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
<div
|
||||
v-for="program in programs.data"
|
||||
@click="openForm(program.name)"
|
||||
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer space-y-2"
|
||||
>
|
||||
<div class="text-lg font-semibold">
|
||||
{{ program.name }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
<span>
|
||||
{{ program.course_count }}
|
||||
{{ program.course_count == 1 ? __('Course') : __('Courses') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<User class="h-4 w-4 stroke-1.5 mr-1" />
|
||||
<span>
|
||||
{{ program.member_count || 0 }}
|
||||
{{ program.member_count == 1 ? __('member') : __('members') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StudentPrograms v-else-if="isStudent" />
|
||||
<EmptyState v-else type="Programs" />
|
||||
|
||||
<ProgramForm
|
||||
v-model="showForm"
|
||||
:programName="currentProgram"
|
||||
@@ -48,11 +50,12 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, Button, usePageMeta, createListResource } from 'frappe-ui'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { BookOpen, Plus, User } from 'lucide-vue-next'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import { sessionStore } from '../../stores/session'
|
||||
import ProgramForm from '@/pages/Programs/ProgramForm.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import StudentPrograms from '@/pages/Programs/StudentPrograms.vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const user = inject('$user')
|
||||
@@ -60,6 +63,15 @@ const showForm = ref(false)
|
||||
const currentProgram = ref(null)
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
if (user.data?.is_moderator || user.data?.is_instructor) {
|
||||
programs.reload()
|
||||
}
|
||||
})
|
||||
|
||||
const programs = createListResource({
|
||||
doctype: 'LMS Program',
|
||||
cache: ['program'],
|
||||
@@ -72,7 +84,7 @@ const programs = createListResource({
|
||||
'enforce_course_order',
|
||||
'allow_self_enrollment',
|
||||
],
|
||||
auto: true,
|
||||
auto: false,
|
||||
orderBy: 'creation desc',
|
||||
})
|
||||
|
||||
@@ -88,6 +100,10 @@ const openForm = (programName) => {
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
const isStudent = computed(() => {
|
||||
return user.data?.is_student || false
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{
|
||||
label: __('Programs'),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="py-10 w-3/4 mx-auto">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||
{{ __('All Programs') }}
|
||||
</div>
|
||||
<TabButtons v-model="currentTab" :buttons="tabs" class="w-fit" />
|
||||
</div>
|
||||
<div v-for="(data, category) in programs.data">
|
||||
<div v-if="category == currentTab" class="grid grid-cols-3 gap-5">
|
||||
<div
|
||||
v-for="program in data"
|
||||
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer"
|
||||
>
|
||||
<div class="text-lg font-semibold mb-2">
|
||||
{{ program.name }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex items-center space-x-10">
|
||||
<div class="flex items-center space-x-1">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ program.course_count }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<User class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ program.member_count || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex items-center space-x-5 text-sm text-ink-gray-7">
|
||||
<div class="flex items-center space-x-1">
|
||||
<BookOpen class="size-3 stroke-1.5" />
|
||||
<span>
|
||||
{{ program.course_count }}
|
||||
{{ program.course_count == 1 ? __('course') : __('courses') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<User class="size-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ program.member_count || 0 }}
|
||||
{{ program.member_count == 1 ? __('member') : __('members') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="Object.keys(program).includes('progress')" class="mt-5">
|
||||
<ProgressBar :progress="program.progress" />
|
||||
<div class="text-sm mt-1">
|
||||
{{ Math.ceil(program.progress) }}% {{ __('completed') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { createResource, TabButtons } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { BookOpen, User } from 'lucide-vue-next'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
|
||||
const currentTab = ref('enrolled')
|
||||
|
||||
const programs = createResource({
|
||||
url: 'lms.lms.utils.get_programs',
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Enrolled'),
|
||||
value: 'enrolled',
|
||||
},
|
||||
{
|
||||
label: __('Published'),
|
||||
value: 'published',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -93,7 +93,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-13 14:36:59.168945",
|
||||
"modified": "2025-08-18 13:08:04.993241",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program",
|
||||
@@ -135,6 +135,15 @@
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
|
||||
+27
-6
@@ -1919,9 +1919,33 @@ 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"])
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
published_programs = frappe.get_all(
|
||||
"LMS Program",
|
||||
{
|
||||
"published": 1,
|
||||
"allow_self_enrollment": 1,
|
||||
},
|
||||
["name", "course_count", "member_count"],
|
||||
)
|
||||
published_programs = [program for program in published_programs if program not in enrolled_programs]
|
||||
|
||||
return {
|
||||
"enrolled": enrolled_programs,
|
||||
"published": published_programs,
|
||||
}
|
||||
|
||||
|
||||
""" def set_program_details(programs):
|
||||
for program in programs:
|
||||
program_courses = frappe.get_all(
|
||||
"LMS Program Course", {"parent": program.name}, ["course"], order_by="idx"
|
||||
@@ -1939,10 +1963,7 @@ def get_programs():
|
||||
|
||||
previous_progress = details.membership.progress if details.membership else 0
|
||||
program.courses.append(details)
|
||||
|
||||
program.members = frappe.db.count("LMS Program Member", {"parent": program.name})
|
||||
|
||||
return programs
|
||||
"""
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
+2
-1
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user