refactor: course list data

This commit is contained in:
Jannat Patel
2024-10-30 22:12:59 +05:30
parent 19b759e9fb
commit 8640ecf9be
18 changed files with 299 additions and 167 deletions

View File

@@ -30,29 +30,29 @@
</div> </div>
<div class="flex flex-col flex-auto p-4"> <div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<div v-if="course.lesson_count"> <div v-if="course.lessons">
<Tooltip :text="__('Lessons')"> <Tooltip :text="__('Lessons')">
<span class="flex items-center"> <span class="flex items-center">
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" /> <BookOpen class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
{{ course.lesson_count }} {{ course.lessons }}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
<div v-if="course.enrollment_count"> <div v-if="course.enrollments">
<Tooltip :text="__('Enrolled Students')"> <Tooltip :text="__('Enrolled Students')">
<span class="flex items-center"> <span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" /> <Users class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
{{ course.enrollment_count }} {{ course.enrollments }}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
<div v-if="course.avg_rating"> <div v-if="course.rating">
<Tooltip :text="__('Average Rating')"> <Tooltip :text="__('Average Rating')">
<span class="flex items-center"> <span class="flex items-center">
<Star class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" /> <Star class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
{{ course.avg_rating }} {{ course.rating }}
</span> </span>
</Tooltip> </Tooltip>
</div> </div>

View File

@@ -93,21 +93,19 @@
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<BookOpen class="h-5 w-5 stroke-1.5 text-gray-600" /> <BookOpen class="h-5 w-5 stroke-1.5 text-gray-600" />
<span class="ml-2"> <span class="ml-2">
{{ course.data.lesson_count }} {{ __('Lessons') }} {{ course.data.lessons }} {{ __('Lessons') }}
</span> </span>
</div> </div>
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<Users class="h-5 w-5 stroke-1.5 text-gray-600" /> <Users class="h-5 w-5 stroke-1.5 text-gray-600" />
<span class="ml-2"> <span class="ml-2">
{{ course.data.enrollment_count_formatted }} {{ formatAmount(course.data.enrollments) }}
{{ __('Enrolled Students') }} {{ __('Enrolled Students') }}
</span> </span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<Star class="h-5 w-5 stroke-1.5 fill-orange-500 text-gray-50" /> <Star class="h-5 w-5 stroke-1.5 fill-orange-500 text-gray-50" />
<span class="ml-2"> <span class="ml-2"> {{ course.data.rating }} {{ __('Rating') }} </span>
{{ course.data.avg_rating }} {{ __('Rating') }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -116,7 +114,7 @@
import { BookOpen, Users, Star } from 'lucide-vue-next' import { BookOpen, Users, Star } from 'lucide-vue-next'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { Button, createResource } from 'frappe-ui' import { Button, createResource } from 'frappe-ui'
import { showToast } from '@/utils/' import { showToast, formatAmount } from '@/utils/'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'

View File

@@ -76,7 +76,7 @@ const props = defineProps({
required: true, required: true,
}, },
avg_rating: { avg_rating: {
type: Number, type: String,
required: true, required: true,
}, },
membership: { membership: {

View File

@@ -21,7 +21,7 @@
<div class="space-y-2"> <div class="space-y-2">
<div <div
class="flex items-center text-sm font-medium space-x-2 cursor-pointer" class="flex text-sm font-medium space-x-2 cursor-pointer"
@click="openHelpDialog('upload')" @click="openHelpDialog('upload')"
> >
<span class="leading-5"> <span class="leading-5">
@@ -58,9 +58,7 @@
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div <div class="flex items-center text-sm font-medium space-x-2">
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
>
<span> <span>
{{ __('What does include in preview mean?') }} {{ __('What does include in preview mean?') }}
</span> </span>

View File

@@ -32,50 +32,52 @@
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<div> <div class="text-xs text-gray-600 mb-2">
<FileUploader {{ __('Meta Image') }}
v-if="!batch.image" </div>
class="mt-4" <FileUploader
:fileTypes="['image/*']" v-if="!batch.image"
:validateFile="validateFile" :fileTypes="['image/*']"
@success="(file) => saveImage(file)" :validateFile="validateFile"
> @success="(file) => saveImage(file)"
<template v-slot="{ file, progress, uploading, openFileSelector }"> >
<div class="mb-4"> <template v-slot="{ file, progress, uploading, openFileSelector }">
<Button @click="openFileSelector" :loading="uploading">
{{ uploading ? `Uploading ${progress}%` : 'Upload an image' }}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="text-xs text-gray-600 mb-1">
{{ __('Meta Image') }}
</div>
<div class="flex items-center"> <div class="flex items-center">
<div class="border rounded-md p-2 mr-2"> <div class="border rounded-md w-fit py-5 px-20">
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" /> <Image class="size-5 stroke-1 text-gray-700" />
</div> </div>
<div class="flex flex-col"> <div class="ml-4">
<span> <Button @click="openFileSelector">
{{ batch.image.file_name }} {{ __('Upload') }}
</span> </Button>
<span class="text-sm text-gray-500 mt-1"> <div class="mt-2 text-gray-600 text-sm">
{{ getFileSize(batch.image.file_size) }} {{
</span> __(
'Appears when the batch URL is shared on any online platform'
)
}}
</div>
</div>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img :src="batch.image.file_url" class="border rounded-md w-40" />
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div class="mt-2 text-gray-600 text-sm">
{{
__(
'Appears when the batch URL is shared on any online platform'
)
}}
</div> </div>
<X
@click="removeImage()"
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div> </div>
</div> </div>
</div> </div>
<MultiSelect
v-model="instructors"
doctype="User"
:label="__('Instructors')"
/>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<FormControl <FormControl
@@ -83,6 +85,7 @@
:label="__('Description')" :label="__('Description')"
type="textarea" type="textarea"
class="my-4" class="my-4"
:placeholder="__('Short description of the batch')"
/> />
<div> <div>
<label class="block text-sm text-gray-600 mb-1"> <label class="block text-sm text-gray-600 mb-1">
@@ -133,6 +136,7 @@
v-model="batch.timezone" v-model="batch.timezone"
:label="__('Timezone')" :label="__('Timezone')"
type="text" type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4" class="mb-4"
/> />
</div> </div>
@@ -149,6 +153,7 @@
:label="__('Seat Count')" :label="__('Seat Count')"
type="number" type="number"
class="mb-4" class="mb-4"
:placeholder="__('Number of seats available')"
/> />
<FormControl <FormControl
v-model="batch.evaluation_end_date" v-model="batch.evaluation_end_date"
@@ -228,10 +233,9 @@ import {
createResource, createResource,
} from 'frappe-ui' } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getFileSize, showToast } from '../utils' import { showToast } from '../utils'
import { X, FileText } from 'lucide-vue-next' import { Image } from 'lucide-vue-next'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
const router = useRouter() const router = useRouter()

View File

@@ -40,6 +40,7 @@
{{ __('Loading Batches...') }} {{ __('Loading Batches...') }}
</div> </div>
<Tabs <Tabs
v-if="hasBatches"
v-model="tabIndex" v-model="tabIndex"
:tabs="makeTabs" :tabs="makeTabs"
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap" tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
@@ -79,24 +80,63 @@
<BatchCard :batch="batch" /> <BatchCard :batch="batch" />
</router-link> </router-link>
</div> </div>
<div <div v-else class="p-5 italic text-gray-500">
v-else {{ __('No {0} batches').format(tab.label.toLowerCase()) }}
class="grid flex-1 place-items-center text-xl font-medium text-gray-500"
>
<div class="flex flex-col items-center justify-center mt-4">
<div>
{{ __('No {0} batches found').format(tab.label.toLowerCase()) }}
</div>
</div>
</div> </div>
</template> </template>
</Tabs> </Tabs>
<div
v-else-if="
batches.fetched &&
!hasBatches &&
(user.data?.is_instructor || user.data?.is_moderator)
"
class="grid grid-cols-3 p-5"
>
<router-link
:to="{
name: 'BatchForm',
params: {
batchName: 'new',
},
}"
>
<div class="bg-gray-50 py-32 px-5 rounded-md">
<div class="flex flex-col items-center text-center space-y-2">
<Plus
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
/>
<div class="font-medium">
{{ __('Create a Batch') }}
</div>
<span class="text-gray-700 text-sm leading-4">
{{ __('You can link courses and assessments to it.') }}
</span>
</div>
</div>
</router-link>
</div>
<div
v-else-if="batches.fetched && !hasBatches"
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
<div class="text-xl font-medium">
{{ __('No batches found') }}
</div>
<div>
{{
__(
'There are no batches available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { import {
createListResource,
createResource, createResource,
Breadcrumbs, Breadcrumbs,
Button, Button,
@@ -104,13 +144,14 @@ import {
Badge, Badge,
Select, Select,
} from 'frappe-ui' } from 'frappe-ui'
import { Plus } from 'lucide-vue-next' import { BookOpen, Plus } from 'lucide-vue-next'
import BatchCard from '@/components/BatchCard.vue' import BatchCard from '@/components/BatchCard.vue'
import { inject, ref, computed, onMounted, watch } from 'vue' import { inject, ref, computed, onMounted, watch } from 'vue'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
const user = inject('$user') const user = inject('$user')
const currentCategory = ref(null) const currentCategory = ref(null)
const hasBatches = ref(false)
onMounted(() => { onMounted(() => {
let queries = new URLSearchParams(location.search) let queries = new URLSearchParams(location.search)
@@ -119,11 +160,18 @@ onMounted(() => {
} }
}) })
const batches = createListResource({ const batches = createResource({
doctype: 'LMS Batch', doctype: 'LMS Batch',
url: 'lms.lms.utils.get_batches', url: 'lms.lms.utils.get_batches',
cache: ['batches', user?.data?.email], cache: ['batches', user.data?.email],
auto: true, auto: true,
onSuccess(data) {
Object.keys(data).forEach((section) => {
if (data[section].length) {
hasBatches.value = true
}
})
},
}) })
const categories = createResource({ const categories = createResource({

View File

@@ -16,16 +16,16 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<Tooltip <Tooltip
v-if="course.data.avg_rating" v-if="course.data.rating"
:text="__('Average Rating')" :text="__('Average Rating')"
class="flex items-center" class="flex items-center"
> >
<Star class="h-5 w-5 text-gray-100 fill-orange-500" /> <Star class="h-5 w-5 text-gray-100 fill-orange-500" />
<span class="ml-1"> <span class="ml-1">
{{ course.data.avg_rating }} {{ course.data.rating }}
</span> </span>
</Tooltip> </Tooltip>
<span v-if="course.data.avg_rating" class="mx-3">&middot;</span> <span v-if="course.data.rating" class="mx-3">&middot;</span>
<Tooltip <Tooltip
v-if="course.data.enrollment_count" v-if="course.data.enrollment_count"
:text="__('Enrolled Students')" :text="__('Enrolled Students')"
@@ -74,7 +74,7 @@
</div> </div>
<CourseReviews <CourseReviews
:courseName="course.data.name" :courseName="course.data.name"
:avg_rating="course.data.avg_rating" :avg_rating="course.data.rating"
:membership="course.data.membership" :membership="course.data.membership"
/> />
</div> </div>
@@ -116,7 +116,7 @@ const breadcrumbs = computed(() => {
let items = [{ label: 'All Courses', route: { name: 'Courses' } }] let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
items.push({ items.push({
label: course?.data?.title, label: course?.data?.title,
route: { name: 'CourseDetail', params: { course: course?.data?.name } }, route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
}) })
return items return items
}) })

View File

@@ -27,7 +27,11 @@
<FormControl <FormControl
v-model="course.short_introduction" v-model="course.short_introduction"
:label="__('Short Introduction')" :label="__('Short Introduction')"
:placeholder="__('A one line introduction to the course that appears on the course card')" :placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
class="mb-4" class="mb-4"
/> />
<div class="mb-4"> <div class="mb-4">
@@ -61,10 +65,12 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<Button @click="openFileSelector"> <Button @click="openFileSelector">
{{ __("Upload") }} {{ __('Upload') }}
</Button> </Button>
<div class="mt-2 text-gray-600 text-sm"> <div class="mt-2 text-gray-600 text-sm">
{{ __("Appears on the course card in the course list") }} {{
__('Appears on the course card in the course list')
}}
</div> </div>
</div> </div>
</div> </div>
@@ -72,39 +78,29 @@
</FileUploader> </FileUploader>
<div v-else class="mb-4"> <div v-else class="mb-4">
<div class="flex items-center"> <div class="flex items-center">
<img :src="course.course_image.file_url" class="border rounded-md w-40" /> <img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4"> <div class="ml-4">
<Button @click="removeImage()"> <Button @click="removeImage()">
{{ __("Remove") }} {{ __('Remove') }}
</Button> </Button>
<div class="mt-2 text-gray-600 text-sm"> <div class="mt-2 text-gray-600 text-sm">
{{ __("Appears on the course card in the course list") }} {{ __('Appears on the course card in the course list') }}
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<img :src="course.course_image.file_url" class="border rounded-md" />
</div>
<div class="flex flex-col">
<span>
{{ course.course_image.file_name }}
</span>
<span class="text-sm text-gray-500 mt-1">
{{ getFileSize(course.course_image.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div> -->
</div> </div>
</div> </div>
<FormControl <FormControl
v-model="course.video_link" v-model="course.video_link"
:label="__('Preview Video')" :label="__('Preview Video')"
:placeholder="__('Paste the youtube link of a short video introducing the course')" :placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
class="mb-4" class="mb-4"
/> />
<div class="mb-4"> <div class="mb-4">

View File

@@ -8,7 +8,7 @@
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]" :items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
/> />
<div class="flex space-x-2 justify-end"> <div class="flex space-x-2 justify-end">
<div class="w-46 md:w-44"> <div class="w-40 md:w-44">
<FormControl <FormControl
v-if="categories.data?.length" v-if="categories.data?.length"
type="select" type="select"
@@ -48,6 +48,7 @@
</header> </header>
<div class=""> <div class="">
<Tabs <Tabs
v-if="hasCourses"
v-model="tabIndex" v-model="tabIndex"
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap" tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
:tabs="makeTabs" :tabs="makeTabs"
@@ -101,45 +102,57 @@
<CourseCard :course="course" /> <CourseCard :course="course" />
</router-link> </router-link>
</div> </div>
<div v-else-if="user.data?.is_moderator || user.data?.is_instructor" class="grid grid-cols-3 p-5"> <div v-else class="p-5 italic text-gray-500">
<router-link {{ __('No {0} courses').format(tab.label.toLowerCase()) }}
:to="{
name: 'CourseForm',
params: {
courseName: 'new',
},
}"
>
<div class="bg-gray-50 py-32 px-5 rounded-md">
<div class="flex flex-col items-center text-center space-y-2">
<Plus
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
/>
<div class="font-medium">
{{ __("Create a Course") }}
</div>
<span class="text-gray-700 text-sm leading-4">
{{ __("You can add chapters and lessons to it.") }}
</span>
</div>
</div>
</router-link>
</div>
<div
v-else
class="text-center p-5 text-gray-600 mt-20 w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
<div class="text-xl font-medium">
{{ __("No courses found") }}
</div>
<div>
{{ __("There are no courses available at the moment. Keep an eye out, fresh learning experiences are on the way soon!") }}
</div>
</div> </div>
</template> </template>
</Tabs> </Tabs>
<div
v-else-if="
courses.fetched &&
(user.data?.is_moderator || user.data?.is_instructor)
"
class="grid grid-cols-3 p-5"
>
<router-link
:to="{
name: 'CourseForm',
params: {
courseName: 'new',
},
}"
>
<div class="bg-gray-50 py-32 px-5 rounded-md">
<div class="flex flex-col items-center text-center space-y-2">
<Plus
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
/>
<div class="font-medium">
{{ __('Create a Course') }}
</div>
<span class="text-gray-700 text-sm leading-4">
{{ __('You can add chapters and lessons to it.') }}
</span>
</div>
</div>
</router-link>
</div>
<div
v-else-if="courses.fetched && !hasCourses"
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
<div class="text-xl font-medium">
{{ __('No courses found') }}
</div>
<div>
{{
__(
'There are no courses available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -161,7 +174,7 @@ import { updateDocumentTitle } from '@/utils'
const user = inject('$user') const user = inject('$user')
const searchQuery = ref('') const searchQuery = ref('')
const currentCategory = ref(null) const currentCategory = ref(null)
const noCoursesFound = ref(false) const hasCourses = ref(false)
onMounted(() => { onMounted(() => {
let queries = new URLSearchParams(location.search) let queries = new URLSearchParams(location.search)
@@ -173,7 +186,14 @@ onMounted(() => {
const courses = createResource({ const courses = createResource({
url: 'lms.lms.utils.get_courses', url: 'lms.lms.utils.get_courses',
cache: ['courses', user.data?.email], cache: ['courses', user.data?.email],
auto: true auto: true,
onSuccess(data) {
Object.keys(data).forEach((section) => {
if (data[section].length) {
hasCourses.value = true
}
})
},
}) })
const tabIndex = ref(0) const tabIndex = ref(0)

View File

@@ -301,14 +301,14 @@ const breadcrumbs = computed(() => {
let items = [{ label: 'All Courses', route: { name: 'Courses' } }] let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
items.push({ items.push({
label: lesson?.data?.course_title, label: lesson?.data?.course_title,
route: { name: 'CourseDetail', params: { course: props.courseName } }, route: { name: 'CourseDetail', params: { courseName: props.courseName } },
}) })
items.push({ items.push({
label: lesson?.data?.title, label: lesson?.data?.title,
route: { route: {
name: 'Lesson', name: 'Lesson',
params: { params: {
course: props.courseName, courseName: props.courseName,
chapterNumber: props.chapterNumber, chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber, lessonNumber: props.lessonNumber,
}, },

View File

@@ -57,6 +57,15 @@ export function formatNumberIntoCurrency(number, currency) {
return '' return ''
} }
// create a function that formats numbers in thousands to k
export function formatAmount(amount) {
if (amount > 999) {
return (amount / 1000).toFixed(1) + 'k'
}
return amount
}
export function convertToTitleCase(str) { export function convertToTitleCase(str) {
if (!str) { if (!str) {
return '' return ''

View File

@@ -110,7 +110,8 @@ doc_events = {
# --------------- # ---------------
scheduler_events = { scheduler_events = {
"hourly": [ "hourly": [
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals" "lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
"lms.lms.api.update_course_statistics",
], ],
"daily": ["lms.job.doctype.job_opportunity.job_opportunity.update_job_openings"], "daily": ["lms.job.doctype.job_opportunity.job_opportunity.update_job_openings"],
} }

View File

@@ -6,8 +6,9 @@ from frappe.translate import get_all_translations
from frappe import _ from frappe import _
from frappe.query_builder import DocType from frappe.query_builder import DocType
from frappe.query_builder.functions import Count from frappe.query_builder.functions import Count
from frappe.utils import time_diff, now_datetime, get_datetime from frappe.utils import time_diff, now_datetime, get_datetime, flt
from typing import Optional from typing import Optional
from lms.lms.utils import get_average_rating, get_lesson_count
@frappe.whitelist() @frappe.whitelist()
@@ -760,3 +761,23 @@ def get_payment_gateway_details(payment_gateway):
"doctype": doctype, "doctype": doctype,
"docname": docname, "docname": docname,
} }
def update_course_statistics():
courses = frappe.get_all("LMS Course", fields=["name"])
for course in courses:
lessons = get_lesson_count(course.name)
enrollments = frappe.db.count(
"LMS Enrollment", {"course": course.name, "member_type": "Student"}
)
avg_rating = get_average_rating(course.name) or 0
avg_rating = flt(avg_rating, frappe.get_system_settings("float_precision") or 3)
frappe.db.set_value(
"LMS Course",
course.name,
{"lessons": lessons, "enrollments": enrollments, "rating": avg_rating},
)

View File

@@ -48,7 +48,12 @@
"certification_section", "certification_section",
"enable_certification", "enable_certification",
"column_break_rxww", "column_break_rxww",
"expiry" "expiry",
"tab_4_tab",
"statistics_section",
"enrollments",
"lessons",
"rating"
], ],
"fields": [ "fields": [
{ {
@@ -249,6 +254,33 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Category", "label": "Category",
"options": "LMS Category" "options": "LMS Category"
},
{
"fieldname": "tab_4_tab",
"fieldtype": "Tab Break",
"label": "Statistics"
},
{
"fieldname": "statistics_section",
"fieldtype": "Section Break"
},
{
"fieldname": "enrollments",
"fieldtype": "Data",
"label": "Enrollments",
"read_only": 1
},
{
"fieldname": "lessons",
"fieldtype": "Data",
"label": "Lessons",
"read_only": 1
},
{
"fieldname": "rating",
"fieldtype": "Data",
"label": "Rating",
"read_only": 1
} }
], ],
"is_published_field": "published", "is_published_field": "published",
@@ -275,7 +307,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2024-09-21 10:23:58.633912", "modified": "2024-10-30 12:48:41.184763",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -75,7 +75,8 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Course", "label": "Course",
"options": "LMS Course", "options": "LMS Course",
"reqd": 1 "reqd": 1,
"search_index": 1
}, },
{ {
"fieldname": "current_lesson", "fieldname": "current_lesson",
@@ -126,7 +127,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-05-14 14:50:08.405033", "modified": "2024-10-30 12:44:16.103598",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Enrollment", "name": "LMS Enrollment",

View File

@@ -157,7 +157,7 @@ def get_lesson_details(chapter, progress=False):
"file_type", "file_type",
"instructor_notes", "instructor_notes",
"course", "course",
"content" "content",
], ],
as_dict=True, as_dict=True,
) )
@@ -176,10 +176,18 @@ def get_lesson_icon(body, content):
content = json.loads(content) content = json.loads(content)
for block in content.get("blocks"): for block in content.get("blocks"):
if block.get("type") == "upload" and block.get("data").get("file_type").lower() in ["mp4", "webm", "ogg", "mov"]: if block.get("type") == "upload" and block.get("data").get("file_type").lower() in [
"mp4",
"webm",
"ogg",
"mov",
]:
return "icon-youtube" return "icon-youtube"
if block.get("type") == "embed" and block.get("data").get("service") in ["youtube", "vimeo"]: if block.get("type") == "embed" and block.get("data").get("service") in [
"youtube",
"vimeo",
]:
return "icon-youtube" return "icon-youtube"
if block.get("type") == "quiz": if block.get("type") == "quiz":
@@ -197,7 +205,6 @@ def get_lesson_icon(body, content):
return "icon-list" return "icon-list"
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_tags(course): def get_tags(course):
tags = frappe.db.get_value("LMS Course", course, "tags") tags = frappe.db.get_value("LMS Course", course, "tags")
@@ -1039,23 +1046,13 @@ def get_course_details(course):
"currency", "currency",
"amount_usd", "amount_usd",
"enable_certification", "enable_certification",
"lessons",
"enrollments",
"rating",
], ],
as_dict=1, as_dict=1,
) )
course_details.tags = course_details.tags.split(",") if course_details.tags else [] course_details.tags = course_details.tags.split(",") if course_details.tags else []
course_details.lesson_count = get_lesson_count(course_details.name)
course_details.enrollment_count = frappe.db.count(
"LMS Enrollment", {"course": course_details.name, "member_type": "Student"}
)
course_details.enrollment_count_formatted = format_number(
course_details.enrollment_count
)
avg_rating = get_average_rating(course_details.name) or 0
course_details.avg_rating = flt(
avg_rating, frappe.get_system_settings("float_precision") or 3
)
course_details.instructors = get_instructors(course_details.name) course_details.instructors = get_instructors(course_details.name)
if course_details.paid_course: if course_details.paid_course:
@@ -1111,7 +1108,7 @@ def get_categorized_courses(courses):
categories = [live, enrolled, created] categories = [live, enrolled, created]
for category in categories: for category in categories:
category.sort(key=lambda x: x.enrollment_count, reverse=True) category.sort(key=lambda x: x.enrollments, reverse=True)
live.sort(key=lambda x: x.featured, reverse=True) live.sort(key=lambda x: x.featured, reverse=True)

View File

@@ -91,3 +91,4 @@ lms.patches.v2_0.fix_progress_percentage
lms.patches.v2_0.add_discussion_topic_titles lms.patches.v2_0.add_discussion_topic_titles
lms.patches.v2_0.sidebar_settings lms.patches.v2_0.sidebar_settings
lms.patches.v2_0.delete_certificate_request_notification #18-09-2024 lms.patches.v2_0.delete_certificate_request_notification #18-09-2024
lms.patches.v2_0.add_course_statistics #21-10-2024

View File

@@ -0,0 +1,6 @@
import frappe
from lms.lms.api import update_course_statistics
def execute():
update_course_statistics()