feat: lesson page

This commit is contained in:
Jannat Patel
2023-12-15 23:39:15 +05:30
parent e7b6001e5f
commit d2922fd361
15 changed files with 330 additions and 93 deletions

View File

@@ -25,7 +25,8 @@ import UserDropdown from '@/components/UserDropdown.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue' import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import SidebarLink from '@/components/SidebarLink.vue' import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { BookOpen, Users, TrendingUp, Search, Bell, Briefcase, Settings } from 'lucide-vue-next' import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
import { ref } from 'vue'
const links = [ const links = [
{ {
@@ -50,5 +51,5 @@ const links = [
}, },
] ]
const isSidebarCollapsed = useStorage('sidebar_is_collapsed', false) let isSidebarCollapsed = ref(useStorage("sidebar_is_collapsed", false))
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="shadow rounded-md p-4 h-full" style="min-height: 150px;"> <div class="border border-gray-200 rounded-md p-4 h-full" style="min-height: 150px;">
<div class="text-xl font-semibold mb-1"> <div class="text-xl font-semibold mb-1">
{{ batch.title }} {{ batch.title }}
</div> </div>
@@ -9,11 +9,15 @@
<div class="mt-auto"> <div class="mt-auto">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<Calendar class="h-4 w-4 stroke-1 mr-2" /> <Calendar class="h-4 w-4 stroke-1 mr-2" />
{{ dayjs(batch.start_date).format("DD MMM YYYY") }} - {{ dayjs(batch.end_date).format("DD MMM YYYY") }} <span>
{{ dayjs(batch.start_date).format("DD MMM YYYY") }} - {{ dayjs(batch.end_date).format("DD MMM YYYY") }}
</span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<Clock class="h-4 w-4 stroke-1 mr-2" /> <Clock class="h-4 w-4 stroke-1 mr-2" />
{{ batch.start_time }} - {{ batch.end_time }} <span>
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -29,6 +33,23 @@ const props = defineProps({
default: null, default: null,
}, },
}); });
function formatTime(timeString) {
if (!timeString) return "";
const [hour, minute] = timeString.split(":").map(Number);
// Create a Date object with dummy values for day, month, and year
const dummyDate = new Date(0, 0, 0, hour, minute);
// Use Intl.DateTimeFormat to format the time in 12-hour format
const formattedTime = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
}).format(dummyDate);
return formattedTime;
}
</script> </script>
<style> <style>
.short-introduction { .short-introduction {

View File

@@ -0,0 +1,11 @@
<template>
</template>
<script setup>
const props = defineProps({
batchName: {
type: Object,
required: true,
},
})
</script>

View File

@@ -2,7 +2,12 @@
<div class="shadow rounded-md" style="width: 300px;"> <div class="shadow rounded-md" style="width: 300px;">
<iframe v-if="course.data.video_link" :src="video_link" class="rounded-t-md" /> <iframe v-if="course.data.video_link" :src="video_link" class="rounded-t-md" />
<div class="p-5"> <div class="p-5">
<Button variant="solid" class="w-full mb-3"> <Button v-if="course.data.membership" variant="solid" class="w-full mb-3">
<span>
{{ __("Continue Learning") }}
</span>
</Button>
<Button v-else variant="solid" class="w-full mb-3" >
<span> <span>
{{ __("Start Learning") }} {{ __("Start Learning") }}
</span> </span>
@@ -31,6 +36,7 @@
<script setup> <script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next' import { BookOpen, Users, Star } from 'lucide-vue-next'
import { computed } from 'vue' import { computed } from 'vue'
import { Button } from "frappe-ui"
const props = defineProps({ const props = defineProps({
course: { course: {
type: Object, type: Object,

View File

@@ -1,24 +1,19 @@
<template> <template>
<div class="text-base mt-10"> <div class="text-base">
<div class="text-2xl font-semibold">
{{ __("Course Content") }}
</div>
<div class="mt-4"> <div class="mt-4">
<Disclosure v-slot="{ open }" v-for="chapter in outline.data" :key="chapter.name"> <Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name">
<DisclosureButton <DisclosureButton class="flex w-full px-2 pt-2 pb-3">
class="flex w-full px-2 pt-2 pb-2" <ChevronRight
> :class="{'rotate-90 transform duration-200' : open, 'duration-200' : !open, 'open': index == 1}"
<ChevronUp
:class="open ? 'rotate-180 transform' : ''"
class="h-5 w-5 text-gray-900 stroke-1 mr-2" class="h-5 w-5 text-gray-900 stroke-1 mr-2"
/> />
<div class="text-lg font-medium"> <div class="text-lg font-medium">
{{ chapter.title }} {{ chapter.title }}
</div> </div>
</DisclosureButton> </DisclosureButton>
<DisclosurePanel class="px-10 pb-2"> <DisclosurePanel class="px-10 pb-4" :static="index == 0">
<div v-for="lesson in chapter.lessons" :key="lesson.name"> <div v-for="lesson in chapter.lessons" :key="lesson.name">
<div class="flex items-center text-lg mb-4"> <div class="flex items-center text-base mb-2">
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/> <MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/> <HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/> <FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
@@ -33,7 +28,7 @@
<script setup> <script setup>
import { createResource } from "frappe-ui"; import { createResource } from "frappe-ui";
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'; import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
import { ChevronUp, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next'; import { ChevronRight, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
const props = defineProps({ const props = defineProps({
courseName: { courseName: {

View File

@@ -8,8 +8,13 @@ import dayjs from '@/utils/dayjs'
import translationPlugin from './translation' import translationPlugin from './translation'
import { usersStore } from './stores/user' import { usersStore } from './stores/user'
import { sessionStore } from './stores/session' import { sessionStore } from './stores/session'
import {
import { FrappeUI, setConfig, frappeRequest, resourcesPlugin } from 'frappe-ui' FrappeUI,
setConfig,
frappeRequest,
resourcesPlugin,
pageMetaPlugin,
} from 'frappe-ui'
let pinia = createPinia() let pinia = createPinia()
let app = createApp(App) let app = createApp(App)
@@ -20,8 +25,8 @@ app.use(pinia)
app.use(router) app.use(router)
app.use(resourcesPlugin) app.use(resourcesPlugin)
app.use(translationPlugin) app.use(translationPlugin)
app.use(pageMetaPlugin)
app.provide('$dayjs', dayjs) app.provide('$dayjs', dayjs)
app.mount('#app') app.mount('#app')
const { userResource } = usersStore() const { userResource } = usersStore()

View File

@@ -13,22 +13,81 @@
</Button> </Button>
</div> </div>
</header> </header>
<div class="mx-5 my-10"> <div class="mx-5 py-5">
<div class="grid grid-cols-4 gap-8 mt-5"> <Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
<BatchCard v-for="batch in batches.data" :batch="batch" /> <template #tab="{ tab, selected }">
</div> <div>
<button
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
:class="{ 'text-gray-900': selected }">
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
{{ __(tab.label) }}
<Badge :class="{ 'text-gray-900 border border-gray-900': selected }" variant="subtle" theme="gray"
size="sm">
{{ tab.count }}
</Badge>
</button>
</div>
</template>
<template #default="{ tab }">
<div v-if="tab.batches && tab.batches.value.length" class="grid grid-cols-1 sm:grid-cols-3 md:grid-cols-4 gap-8 mt-5">
<router-link v-for="batch in tab.batches.value"
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }">
<BatchCard :batch="batch" />
</router-link>
</div>
<div v-else 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>
</template>
</Tabs>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, Breadcrumbs } from "frappe-ui"; import { createResource, Breadcrumbs, Button, Tabs, Badge } from "frappe-ui";
import { Plus } from "lucide-vue-next" import { Plus } from "lucide-vue-next"
import BatchCard from '@/components/BatchCard.vue'; import BatchCard from '@/components/BatchCard.vue';
import { inject, ref, computed } from "vue";
const user = inject("$user")
const batches = createResource({ const batches = createResource({
url: "lms.lms.utils.get_batches", url: "lms.lms.utils.get_batches",
cache: ["batches"], cache: ["batches", user?.data?.email],
auto: true, auto: true,
}); });
console.log(batches)
const tabIndex = ref(0)
const tabs = [
{
label: "Upcoming",
batches: computed(() => batches.data?.upcoming || []),
count: computed(() => batches.data?.upcoming?.length),
},
];
if (user.data?.is_moderator) {
tabs.push({
label: "Archived",
batches: computed(() => batches.data?.archived),
count: computed(() => batches.data?.archived?.length),
});
tabs.push({
label: "Private",
batches: computed(() => batches.data?.private),
count: computed(() => batches.data?.private?.length),
})
}
if (user.data) {
tabs.push({
label: "Enrolled",
batches: computed(() => batches.data?.enrolled),
count: computed(() => batches.data?.enrolled?.length)
})
}
</script> </script>

View File

@@ -30,7 +30,9 @@
</div> </div>
&middot; &middot;
<div class="flex items-center"> <div class="flex items-center">
<BookOpen class="h-4 w-4 text-gray-700 mr-1"/> <span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor"/>
</span>
<span v-if="course.data.instructors.length == 1"> <span v-if="course.data.instructors.length == 1">
{{ course.data.instructors[0].full_name }} {{ course.data.instructors[0].full_name }}
</span> </span>
@@ -46,7 +48,12 @@
<div class="grid grid-cols-[60%,20%] gap-20 mt-10"> <div class="grid grid-cols-[60%,20%] gap-20 mt-10">
<div class=""> <div class="">
<div v-html="course.data.description" class="course-description"></div> <div v-html="course.data.description" class="course-description"></div>
<CourseOutline :courseName="course.data.name"/> <div class="mt-10">
<div class="text-2xl font-semibold">
{{ __("Course Content") }}
</div>
<CourseOutline :courseName="course.data.name"/>
</div>
<CourseReviews :courseName="course.data.name" :avg_rating="course.data.avg_rating"/> <CourseReviews :courseName="course.data.name" :avg_rating="course.data.avg_rating"/>
</div> </div>
<div> <div>
@@ -59,10 +66,11 @@
<script setup> <script setup>
import { createResource, Breadcrumbs } from "frappe-ui"; import { createResource, Breadcrumbs } from "frappe-ui";
import { computed } from "vue"; import { computed } from "vue";
import { BookOpen, Users, Star } from 'lucide-vue-next' import { Users, Star } from 'lucide-vue-next'
import CourseCardOverlay from '@/components/CourseCardOverlay.vue'; import CourseCardOverlay from '@/components/CourseCardOverlay.vue';
import CourseOutline from '@/components/CourseOutline.vue'; import CourseOutline from '@/components/CourseOutline.vue';
import CourseReviews from '@/components/CourseReviews.vue'; import CourseReviews from '@/components/CourseReviews.vue';
import UserAvatar from '@/components/UserAvatar.vue'
const props = defineProps({ const props = defineProps({
courseName: { courseName: {
@@ -103,4 +111,13 @@ const breadcrumbs = computed(() => {
margin: revert; margin: revert;
padding: revert; padding: revert;
} }
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
</style> </style>

View File

@@ -3,7 +3,6 @@
<header class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"> <header class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5">
<Breadcrumbs class="h-7" :items="[{ label: __('All Courses'), route: { name: 'Courses' } }]" /> <Breadcrumbs class="h-7" :items="[{ label: __('All Courses'), route: { name: 'Courses' } }]" />
<div class="flex"> <div class="flex">
<Select class="mr-2" :options="orderOptions" v-model="orderBy" />
<Button variant="solid"> <Button variant="solid">
<template #prefix> <template #prefix>
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />
@@ -29,7 +28,7 @@
</div> </div>
</template> </template>
<template #default="{ tab }"> <template #default="{ tab }">
<div v-if="tab.courses && tab.courses.value.length" class="grid grid-cols-3 gap-8 mt-5"> <div v-if="tab.courses && tab.courses.value.length" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">
<router-link v-for="course in tab.courses.value" <router-link v-for="course in tab.courses.value"
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"> :to="{ name: 'CourseDetail', params: { courseName: course.name } }">
<CourseCard :course="course" /> <CourseCard :course="course" />
@@ -50,13 +49,12 @@
<script setup> <script setup>
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { createListResource, Breadcrumbs, Tabs, Badge, Select, Button } from 'frappe-ui'; import { createListResource, Breadcrumbs, Tabs, Badge, Button } from 'frappe-ui';
import CourseCard from '@/components/CourseCard.vue'; import CourseCard from '@/components/CourseCard.vue';
import { Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { ref, computed, inject } from 'vue' import { ref, computed, inject } from 'vue'
const user = inject("$user") const user = inject("$user")
const courses = createListResource({ const courses = createListResource({
type: 'list', type: 'list',
doctype: 'LMS Course', doctype: 'LMS Course',
@@ -65,17 +63,6 @@ const courses = createListResource({
auto: true, auto: true,
}); });
const is_moderator = computed(() => {
if (user.data?.roles?.includes('Moderator')) {
return true;
}
return false;
});
const is_instructor = computed(() => {
return user.data.roles.includes("Course Creator") ? true : false;
});
const tabIndex = ref(0) const tabIndex = ref(0)
const tabs = [ const tabs = [
{ {
@@ -89,7 +76,7 @@ const tabs = [
count: computed(() => courses.data?.upcoming?.length), count: computed(() => courses.data?.upcoming?.length),
} }
]; ];
console.log(user.data)
if (user.data) { if (user.data) {
tabs.push({ tabs.push({
label: 'Enrolled', label: 'Enrolled',
@@ -97,7 +84,7 @@ if (user.data) {
count: computed(() => courses.data?.enrolled?.length), count: computed(() => courses.data?.enrolled?.length),
}); });
if (is_moderator.value || is_instructor.value || courses.data?.created?.length) { if (user.data.is_moderator || user.data.is_instructor || courses.data?.created?.length) {
tabs.push({ tabs.push({
label: 'Created', label: 'Created',
courses: computed(() => courses.data?.created), courses: computed(() => courses.data?.created),
@@ -105,7 +92,7 @@ if (user.data) {
}); });
}; };
if (is_moderator.value) { if (user.data.is_moderator) {
tabs.push({ tabs.push({
label: 'Under Review', label: 'Under Review',
courses: computed(() => courses.data?.under_review), courses: computed(() => courses.data?.under_review),
@@ -113,25 +100,4 @@ if (user.data) {
}); });
} }
}; };
const orderOptions = [
{
label: "Sort By",
disabled: 1
},
{
label: "Most Popular",
value: "enrollment"
},
{
label: "Highest Rated",
value: "rating"
},
{
label: "Newest",
value: "creation"
},
];
const orderBy = 'enrollment';
</script> </script>

View File

@@ -1,29 +1,135 @@
<template> <template>
Lesson Page <div v-if="lesson.data && course.data" class="h-screen text-base">
<header class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5">
<Breadcrumbs class="h-7" :items="breadcrumbs"/>
</header>
<div class="grid grid-cols-[70%,30%] h-full">
<div class="border-r-2 container pt-5 pb-10">
<div class="text-3xl font-semibold">
{{ lesson.data.title }}
</div>
<div class="flex items-center mt-2">
<span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor"/>
</span>
<span v-if="course.data.instructors.length == 1">
{{ course.data.instructors[0].full_name }}
</span>
<span v-if="course.data.instructors.length == 2">
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors[1].first_name }}
</span>
<span v-if="course.data.instructors.length > 2">
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors.length - 1 }} others
</span>
</div>
<div v-html="lesson.data.rendered_content" class="lesson-content mt-6"></div>
</div>
<div class="sticky top-10">
<div class="bg-gray-50 p-5 border-b-2">
<div class="text-lg font-semibold">
{{ course.data.title }}
</div>
<div v-if="user && course.data.membership" class="text-sm mt-3">
{{ Math.ceil(course.data.membership.progress) }}% completed
</div>
<div v-if="user && course.data.membership" class="w-full bg-gray-200 rounded-full h-1 my-2">
<div class="bg-gray-900 h-1 rounded-full" :style="{ width: Math.ceil(course.data.membership.progress) + '%' }"></div>
</div>
</div>
<CourseOutline :courseName="lesson.data.course"/>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { createResource, Button } from "frappe-ui"; import { createResource, Breadcrumbs } from "frappe-ui";
import { useRoute } from "vue-router"; import { computed, onMounted, onBeforeMount, onUnmounted, inject } from "vue";
const route = useRoute(); import { useStorage } from '@vueuse/core'
console.log(route) import CourseOutline from '@/components/CourseOutline.vue';
import UserAvatar from '@/components/UserAvatar.vue';
const user = inject("$user");
onBeforeMount(() => {
console.log("before mount");
localStorage.setItem("sidebar_is_collapsed", true);
})
const props = defineProps({ const props = defineProps({
courseName: { courseName: {
type: String, type: String,
required: true, required: true,
}, },
chapterNumber: {
type: String,
required: true,
},
lessonNumber: { lessonNumber: {
type: Number, type: String,
required: true, required: true,
}, },
}); });
/*
const lesson = createResource({ const lesson = createResource({
url: "lms.lms.utils.get_lesson", url: "lms.lms.utils.get_lesson",
cache: ["lesson", props.courseName, props.lessonNumber], cache: ["lesson", props.courseName, props.lessonNumber],
params: { params: {
course: props.courseName, course: props.courseName,
chapter: props.chapterNumber,
lesson: props.lessonNumber, lesson: props.lessonNumber,
}, },
auto: true, auto: true,
}); */ });
</script>
const course = createResource({
url: "lms.lms.utils.get_course_details",
cache: ["course", props.courseName],
params: {
course: props.courseName
},
auto: true,
});
const breadcrumbs = computed(() => {
let items = [{ label: "All Courses", route: { name: "Courses" } }]
items.push({
label: course?.data?.title,
route: { name: "CourseDetail", params: { course: props.courseName } },
})
items.push({
label: lesson?.data?.title,
route: { name: "Lesson", params: { course: props.courseName, chapterNumber: props.chapterNumber, lessonNumber: props.lessonNumber } },
})
return items
});
onUnmounted(() => {
console.log("unmounted");
useStorage("sidebar_is_collapsed", false);
});
</script>
<style>
.youtube-video {
border: 1px solid #ddd;
}
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
iframe {
border-radius: 0.5rem;
}
.lesson-content div {
margin-bottom: 1rem;
}
.lesson-content p {
margin-bottom: 1rem;
line-height: 1.7;
}
</style>

View File

@@ -21,16 +21,21 @@ const routes = [
}, },
{ {
// Create a route for path /courses/inventory-management/learn/1.1 // Create a route for path /courses/inventory-management/learn/1.1
path: '/courses/:courseName/learn/:lessonNumber', path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber',
name: 'Lesson', name: 'Lesson',
component: () => import('@/pages/Lesson.vue'), component: () => import('@/pages/Lesson.vue'),
props: {}, props: true,
}, },
{ {
path: '/batches', path: '/batches',
name: 'Batches', name: 'Batches',
component: () => import('@/pages/Batches.vue'), component: () => import('@/pages/Batches.vue'),
}, },
{
path: '/batches/:batchName',
name: 'BatchDetail',
component: () => import('@/pages/BatchDetail.vue'),
},
] ]
let router = createRouter({ let router = createRouter({

View File

@@ -154,6 +154,8 @@ def get_user_info():
as_dict=1, as_dict=1,
) )
user["roles"] = frappe.get_roles(user.name) user["roles"] = frappe.get_roles(user.name)
user.is_instructor = "Course Creator" in user.roles
user.is_moderator = "Moderator" in user.roles
return user return user

View File

@@ -25,6 +25,8 @@ from frappe.utils import (
validate_phone_number, validate_phone_number,
get_fullname, get_fullname,
pretty_date, pretty_date,
get_time_str,
nowtime,
) )
from frappe.utils.dateutils import get_period from frappe.utils.dateutils import get_period
from lms.lms.md import find_macros, markdown_to_html from lms.lms.md import find_macros, markdown_to_html
@@ -1226,15 +1228,12 @@ def get_categorized_courses(courses):
if course.membership and course.published: if course.membership and course.published:
enrolled.append(course) enrolled.append(course)
elif course.is_instructor: elif course.is_instructor:
print(course.name)
print(course.enrollment_count)
created.append(course) created.append(course)
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.enrollment_count, reverse=True)
print(created)
return { return {
"live": live, "live": live,
"upcoming": upcoming, "upcoming": upcoming,
@@ -1264,24 +1263,32 @@ def get_course_outline(course):
@frappe.whitelist() @frappe.whitelist()
def get_lesson(lesson): def get_lesson(course, chapter, lesson):
chapter_name = frappe.db.get_value(
"Chapter Reference", {"parent": course, "idx": chapter}, "chapter"
)
lesson_name = frappe.db.get_value(
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
)
lesson = frappe.db.get_value( lesson = frappe.db.get_value(
"Course Lesson", "Course Lesson",
lesson, lesson_name,
[ [
"name", "name",
"title", "title",
"description", "include_in_preview",
"idx",
"video_link",
"body", "body",
"creation",
"youtube", "youtube",
"quiz_id", "quiz_id",
"question", "question",
"file_type", "file_type",
"instructor_notes",
"course",
], ],
as_dict=True, as_dict=True,
) )
lesson.rendered_content = render_html(lesson)
return lesson return lesson
@@ -1290,6 +1297,7 @@ def get_batches():
batches = frappe.get_all( batches = frappe.get_all(
"LMS Batch", "LMS Batch",
fields=[ fields=[
"name",
"title", "title",
"description", "description",
"start_date", "start_date",
@@ -1297,7 +1305,42 @@ def get_batches():
"start_time", "start_time",
"end_time", "end_time",
"seat_count", "seat_count",
"published",
], ],
) )
batches = categorize_batches(batches)
return batches return batches
def categorize_batches(batches):
upcoming, archived, private, enrolled = [], [], [], []
for batch in batches:
if not batch.published:
private.append(batch)
elif getdate(batch.start_date) < getdate():
archived.append(batch)
elif (
getdate(batch.start_date) == getdate() and get_time_str(batch.start_time) < nowtime()
):
archived.append(batch)
else:
upcoming.append(batch)
if frappe.session.user != "Guest":
if frappe.db.exists(
"Batch Student", {"student": frappe.session.user, "parent": batch.name}
):
enrolled.append(batch)
categories = [archived, private, enrolled]
for category in categories:
category.sort(key=lambda x: x.start_date, reverse=True)
upcoming.sort(key=lambda x: x.start_date)
return {
"upcoming": upcoming,
"archived": archived,
"private": private,
"enrolled": enrolled,
}

View File

@@ -180,7 +180,7 @@ def youtube_video_renderer(video_id):
src="https://www.youtube.com/embed/{video_id}" src="https://www.youtube.com/embed/{video_id}"
title="YouTube video player" title="YouTube video player"
frameborder="0" frameborder="0"
style="border-radius: var(--border-radius-lg)" class="youtube-video
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen> allowfullscreen>
</iframe> </iframe>

View File

@@ -71,7 +71,7 @@
</span> </span>
</a> </a>
<div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}"> <div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}">
{{ frappe.utils.pretty_date(review.creation) }} {{ review.creation }}
</div> </div>
</div> </div>
<div class="rating"> <div class="rating">