feat: record lesson progress
This commit is contained in:
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://test:8000",
|
baseUrl: "http://lms1:8000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
|
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
|
||||||
:class="isSidebarCollapsed ? 'w-14' : 'w-56'"
|
:class="sidebarStore.isSidebarCollapsed ? 'w-14' : 'w-56'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col overflow-hidden"
|
class="flex flex-col overflow-hidden"
|
||||||
:class="isSidebarCollapsed ? 'items-center' : ''"
|
:class="sidebarStore.isSidebarCollapsed ? 'items-center' : ''"
|
||||||
>
|
>
|
||||||
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
<UserDropdown :isCollapsed="sidebarStore.isSidebarCollapsed" />
|
||||||
<div class="flex flex-col" v-if="sidebarSettings.data">
|
<div class="flex flex-col" v-if="sidebarSettings.data">
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
v-for="link in sidebarLinks"
|
v-for="link in sidebarLinks"
|
||||||
:link="link"
|
:link="link"
|
||||||
:isCollapsed="isSidebarCollapsed"
|
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
class="mx-2 my-0.5"
|
class="mx-2 my-0.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between pr-2 cursor-pointer"
|
class="flex items-center justify-between pr-2 cursor-pointer"
|
||||||
:class="isSidebarCollapsed ? 'pl-3' : 'pl-4'"
|
:class="sidebarStore.isSidebarCollapsed ? 'pl-3' : 'pl-4'"
|
||||||
@click="showWebPages = !showWebPages"
|
@click="showWebPages = !showWebPages"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isSidebarCollapsed"
|
v-if="!sidebarStore.isSidebarCollapsed"
|
||||||
class="flex items-center text-sm text-gray-600 my-1"
|
class="flex items-center text-sm text-gray-600 my-1"
|
||||||
>
|
>
|
||||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<SidebarLink
|
<SidebarLink
|
||||||
v-for="link in sidebarSettings.data.web_pages"
|
v-for="link in sidebarSettings.data.web_pages"
|
||||||
:link="link"
|
:link="link"
|
||||||
:isCollapsed="isSidebarCollapsed"
|
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
class="mx-2 my-0.5"
|
class="mx-2 my-0.5"
|
||||||
:showControls="isModerator ? true : false"
|
:showControls="isModerator ? true : false"
|
||||||
@openModal="openPageModal"
|
@openModal="openPageModal"
|
||||||
@@ -64,17 +64,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
:link="{
|
:link="{
|
||||||
label: isSidebarCollapsed ? 'Expand' : 'Collapse',
|
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||||
}"
|
}"
|
||||||
:isCollapsed="isSidebarCollapsed"
|
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
@click="isSidebarCollapsed = !isSidebarCollapsed"
|
@click="toggleSidebar()"
|
||||||
class="m-2"
|
class="m-2"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||||
<CollapseSidebar
|
<CollapseSidebar
|
||||||
class="h-4.5 w-4.5 text-gray-700 duration-300 ease-in-out"
|
class="h-4.5 w-4.5 text-gray-700 duration-300 ease-in-out"
|
||||||
:class="{ '[transform:rotateY(180deg)]': isSidebarCollapsed }"
|
:class="{
|
||||||
|
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -96,12 +98,14 @@ import { ref, onMounted, inject, watch } from 'vue'
|
|||||||
import { getSidebarLinks } from '../utils'
|
import { getSidebarLinks } from '../utils'
|
||||||
import { usersStore } from '@/stores/user'
|
import { usersStore } from '@/stores/user'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import { useSidebar } from '@/stores/sidebar'
|
||||||
import { ChevronRight, Plus } from 'lucide-vue-next'
|
import { ChevronRight, Plus } from 'lucide-vue-next'
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { createResource, Button } from 'frappe-ui'
|
||||||
import PageModal from '@/components/Modals/PageModal.vue'
|
import PageModal from '@/components/Modals/PageModal.vue'
|
||||||
|
|
||||||
const { user, sidebarSettings } = sessionStore()
|
const { user, sidebarSettings } = sessionStore()
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
|
let sidebarStore = useSidebar()
|
||||||
const socket = inject('$socket')
|
const socket = inject('$socket')
|
||||||
const unreadCount = ref(0)
|
const unreadCount = ref(0)
|
||||||
const sidebarLinks = ref(getSidebarLinks())
|
const sidebarLinks = ref(getSidebarLinks())
|
||||||
@@ -214,5 +218,8 @@ watch(userResource, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let isSidebarCollapsed = ref(getSidebarFromStorage())
|
const toggleSidebar = () => {
|
||||||
|
console.log(sidebarStore.isSidebarCollapsed)
|
||||||
|
sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function enrollStudent() {
|
|||||||
showToast(
|
showToast(
|
||||||
__('Please Login'),
|
__('Please Login'),
|
||||||
__('You need to login first to enroll for this course'),
|
__('You need to login first to enroll for this course'),
|
||||||
'circle-warn'
|
'alert-circle'
|
||||||
)
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
'shadow rounded-md pt-2 px-2': showOutline && outline.data?.length,
|
'shadow rounded-md py-2 px-2': showOutline && outline.data?.length,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Disclosure
|
<Disclosure
|
||||||
@@ -25,21 +25,42 @@
|
|||||||
:key="chapter.name"
|
:key="chapter.name"
|
||||||
:defaultOpen="openChapterDetail(chapter.idx)"
|
:defaultOpen="openChapterDetail(chapter.idx)"
|
||||||
>
|
>
|
||||||
<DisclosureButton ref="" class="flex w-full p-2">
|
<DisclosureButton ref="" class="flex items-center w-full p-2 group">
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
:class="{
|
:class="{
|
||||||
'rotate-90 transform duration-200': open,
|
'rotate-90 transform duration-200': open,
|
||||||
'duration-200': !open,
|
'duration-200': !open,
|
||||||
|
hidden: chapter.is_scorm_package,
|
||||||
open: index == 1,
|
open: index == 1,
|
||||||
}"
|
}"
|
||||||
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
|
class="h-4 w-4 text-gray-900 stroke-1"
|
||||||
/>
|
/>
|
||||||
<div class="text-base text-left font-medium leading-5">
|
<div
|
||||||
|
class="text-base text-left font-medium leading-5 ml-2"
|
||||||
|
@click="redirectToChapter(chapter)"
|
||||||
|
>
|
||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex ml-auto space-x-4">
|
||||||
|
<Tooltip :text="__('Edit Chapter')" placement="bottom">
|
||||||
|
<FilePenLine
|
||||||
|
v-if="allowEdit"
|
||||||
|
@click.prevent="openChapterModal(chapter)"
|
||||||
|
class="h-4 w-4 text-gray-900 invisible group-hover:visible"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :text="__('Delete Chapter')" placement="bottom">
|
||||||
|
<Trash2
|
||||||
|
v-if="allowEdit"
|
||||||
|
@click.prevent="trashChapter(chapter.name)"
|
||||||
|
class="h-4 w-4 text-red-500 invisible group-hover:visible"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</DisclosureButton>
|
</DisclosureButton>
|
||||||
<DisclosurePanel>
|
<DisclosurePanel v-if="!chapter.is_scorm_package">
|
||||||
<Draggable
|
<Draggable
|
||||||
|
v-if="!chapter.is_scorm_package"
|
||||||
:list="chapter.lessons"
|
:list="chapter.lessons"
|
||||||
:disabled="!allowEdit"
|
:disabled="!allowEdit"
|
||||||
item-key="name"
|
item-key="name"
|
||||||
@@ -103,9 +124,6 @@
|
|||||||
{{ __('Add Lesson') }}
|
{{ __('Add Lesson') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<Button class="ml-2" @click="openChapterModal(chapter)">
|
|
||||||
{{ __('Edit Chapter') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
@@ -119,24 +137,26 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||||
import { ref, getCurrentInstance } from 'vue'
|
import { getCurrentInstance, inject, ref } from 'vue'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
|
||||||
MonitorPlay,
|
|
||||||
HelpCircle,
|
|
||||||
FileText,
|
|
||||||
Check,
|
Check,
|
||||||
|
ChevronRight,
|
||||||
|
FileText,
|
||||||
|
FilePenLine,
|
||||||
|
HelpCircle,
|
||||||
|
MonitorPlay,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
||||||
import { showToast } from '@/utils'
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const expandAll = ref(true)
|
const router = useRouter()
|
||||||
|
const user = inject('$user')
|
||||||
const showChapterModal = ref(false)
|
const showChapterModal = ref(false)
|
||||||
const currentChapter = ref(null)
|
const currentChapter = ref(null)
|
||||||
const app = getCurrentInstance()
|
const app = getCurrentInstance()
|
||||||
@@ -206,8 +226,10 @@ const updateLessonIndex = createResource({
|
|||||||
|
|
||||||
const trashLesson = (lessonName, chapterName) => {
|
const trashLesson = (lessonName, chapterName) => {
|
||||||
$dialog({
|
$dialog({
|
||||||
title: __('Delete Lesson'),
|
title: __('Delete this lesson?'),
|
||||||
message: __('Are you sure you want to delete this lesson?'),
|
message: __(
|
||||||
|
'Deleting this lesson will permanently remove it from the course. This action cannot be undone. Are you sure you want to continue?'
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: __('Delete'),
|
label: __('Delete'),
|
||||||
@@ -246,6 +268,61 @@ const updateOutline = (e) => {
|
|||||||
idx: e.newIndex,
|
idx: e.newIndex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteChapter = createResource({
|
||||||
|
url: 'lms.lms.api.delete_chapter',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
chapter: values.chapter,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
outline.reload()
|
||||||
|
showToast('Success', 'Chapter deleted successfully', 'check')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const trashChapter = (chapterName) => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Delete this chapter?'),
|
||||||
|
message: __(
|
||||||
|
'Deleting this chapter will also delete all its lessons and permanently remove it from the course. This action cannot be undone. Are you sure you want to continue?'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Delete'),
|
||||||
|
theme: 'red',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(close) {
|
||||||
|
deleteChapter.submit({ chapter: chapterName })
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectToChapter = (chapter) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (props.allowEdit) return
|
||||||
|
if (!chapter.is_scorm_package) return
|
||||||
|
if (!user.data) {
|
||||||
|
showToast(
|
||||||
|
__('You are not enrolled'),
|
||||||
|
__('Please enroll for this course to view this lesson'),
|
||||||
|
'alert-circle'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'SCORMChapter',
|
||||||
|
params: {
|
||||||
|
courseName: props.courseName,
|
||||||
|
chapterName: chapter.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.outline-lesson:has(.router-link-active) {
|
.outline-lesson:has(.router-link-active) {
|
||||||
|
|||||||
@@ -15,8 +15,13 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4 text-base">
|
||||||
<FormControl ref="chapterInput" label="Title" v-model="chapter.title" />
|
<FormControl
|
||||||
|
ref="chapterInput"
|
||||||
|
label="Title"
|
||||||
|
v-model="chapter.title"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
:label="__('Is SCORM Package')"
|
:label="__('Is SCORM Package')"
|
||||||
v-model="chapter.is_scorm_package"
|
v-model="chapter.is_scorm_package"
|
||||||
@@ -97,25 +102,14 @@ const chapter = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const chapterResource = createResource({
|
const chapterResource = createResource({
|
||||||
url: 'lms.lms.api.add_chapter',
|
url: 'lms.lms.api.upsert_chapter',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
title: chapter.title,
|
title: chapter.title,
|
||||||
course: props.course,
|
course: props.course,
|
||||||
is_scorm_package: chapter.is_scorm_package,
|
is_scorm_package: chapter.is_scorm_package,
|
||||||
scorm_package: chapter.scorm_package,
|
scorm_package: chapter.scorm_package,
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const chapterEditResource = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Course Chapter',
|
|
||||||
name: props.chapterDetail?.name,
|
name: props.chapterDetail?.name,
|
||||||
fieldname: 'title',
|
|
||||||
value: chapter.title,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -140,12 +134,7 @@ const addChapter = async (close) => {
|
|||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
if (!chapter.title) {
|
return validateChapter()
|
||||||
return __('Title is required')
|
|
||||||
}
|
|
||||||
if (chapter.is_scorm_package && !chapter.scorm_package) {
|
|
||||||
return __('Please upload a SCORM package')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
capture('chapter_created')
|
capture('chapter_created')
|
||||||
@@ -175,6 +164,15 @@ const addChapter = async (close) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateChapter = () => {
|
||||||
|
if (!chapter.title) {
|
||||||
|
return __('Title is required')
|
||||||
|
}
|
||||||
|
if (chapter.is_scorm_package && !chapter.scorm_package) {
|
||||||
|
return __('Please upload a SCORM package')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cleanChapter = () => {
|
const cleanChapter = () => {
|
||||||
chapter.title = ''
|
chapter.title = ''
|
||||||
chapter.is_scorm_package = 0
|
chapter.is_scorm_package = 0
|
||||||
@@ -182,7 +180,7 @@ const cleanChapter = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editChapter = (close) => {
|
const editChapter = (close) => {
|
||||||
chapterEditResource.submit(
|
chapterResource.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
@@ -206,6 +204,8 @@ watch(
|
|||||||
() => props.chapterDetail,
|
() => props.chapterDetail,
|
||||||
(newChapter) => {
|
(newChapter) => {
|
||||||
chapter.title = newChapter?.title
|
chapter.title = newChapter?.title
|
||||||
|
chapter.is_scorm_package = newChapter?.is_scorm_package
|
||||||
|
chapter.scorm_package = newChapter?.scorm_package
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,11 @@
|
|||||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal"
|
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal"
|
||||||
></div>
|
></div>
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<CourseOutline :courseName="course.data.name" :showOutline="true" />
|
<CourseOutline
|
||||||
|
:title="__('Course Outline')"
|
||||||
|
:courseName="course.data.name"
|
||||||
|
:showOutline="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CourseReviews
|
<CourseReviews
|
||||||
:courseName="course.data.name"
|
:courseName="course.data.name"
|
||||||
|
|||||||
@@ -244,7 +244,10 @@ const lesson = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (Object.keys(data).length === 0) {
|
if (Object.keys(data).length === 0) {
|
||||||
router.push({ name: 'Courses' })
|
router.push({
|
||||||
|
name: 'CourseDetail',
|
||||||
|
params: { courseName: props.courseName },
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lessonProgress.value = data.membership?.progress
|
lessonProgress.value = data.membership?.progress
|
||||||
|
|||||||
205
frontend/src/pages/SCORMChapter.vue
Normal file
205
frontend/src/pages/SCORMChapter.vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<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
|
||||||
|
v-if="
|
||||||
|
readyToRender &&
|
||||||
|
(enrollment.data?.length ||
|
||||||
|
user.data?.is_moderator ||
|
||||||
|
user.data?.is_instructor)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<iframe :src="chapter.doc.launch_file" class="w-full h-screen" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!enrollment.data?.length">
|
||||||
|
<div class="text-center pt-10 px-5 md:px-0 pb-10">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'You are not enrolled in this course. Please enroll to access this lesson.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<Button variant="solid" @click="enrollStudent()">
|
||||||
|
{{ __('Start Learning') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Breadcrumbs,
|
||||||
|
Button,
|
||||||
|
call,
|
||||||
|
createDocumentResource,
|
||||||
|
createListResource,
|
||||||
|
createResource,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, inject, onBeforeMount, ref } from 'vue'
|
||||||
|
import { useSidebar } from '@/stores/sidebar'
|
||||||
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
|
const sidebarStore = useSidebar()
|
||||||
|
const user = inject('$user')
|
||||||
|
const readyToRender = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
chapterName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
sidebarStore.isSidebarCollapsed = true
|
||||||
|
window.API_1484_11 = {
|
||||||
|
Initialize: () => 'true',
|
||||||
|
Terminate: () => 'true',
|
||||||
|
GetValue: (key) => {
|
||||||
|
console.log(`GetValue called for key: ${key}`)
|
||||||
|
return getDataFromLMS(key)
|
||||||
|
},
|
||||||
|
SetValue: (key, value) => {
|
||||||
|
console.log(`SetValue called for key: ${key} to value: ${value}`)
|
||||||
|
|
||||||
|
saveDataToLMS(key, value)
|
||||||
|
return 'true'
|
||||||
|
},
|
||||||
|
Commit: () => 'true',
|
||||||
|
GetLastError: () => '0',
|
||||||
|
GetErrorString: () => '',
|
||||||
|
GetDiagnostic: () => '',
|
||||||
|
}
|
||||||
|
window.API = {
|
||||||
|
LMSInitialize: () => 'true',
|
||||||
|
LMSFinish: () => 'true',
|
||||||
|
LMSGetValue: (key) => {
|
||||||
|
console.log(`GET: ${key}`)
|
||||||
|
return getDataFromLMS(key)
|
||||||
|
},
|
||||||
|
LMSSetValue: (key, value) => {
|
||||||
|
console.log(`SET: ${key} to value: ${value}`)
|
||||||
|
saveDataToLMS(key, value)
|
||||||
|
return 'true'
|
||||||
|
},
|
||||||
|
LMSCommit: () => 'true',
|
||||||
|
LMSGetLastError: () => '0',
|
||||||
|
LMSGetErrorString: () => '',
|
||||||
|
LMSGetDiagnostic: () => '',
|
||||||
|
}
|
||||||
|
console.log(window.API_1484_11)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDataFromLMS = (key) => {
|
||||||
|
if (key == 'cmi.core.lesson_status') {
|
||||||
|
if (progress.data?.status == 'Complete') {
|
||||||
|
return 'passed'
|
||||||
|
}
|
||||||
|
return 'incomplete'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDataToLMS = (key, value) => {
|
||||||
|
if (key == 'cmi.core.lesson_status' && value == 'passed') {
|
||||||
|
saveProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrollment = createListResource({
|
||||||
|
doctype: 'LMS Enrollment',
|
||||||
|
fields: ['member', 'course'],
|
||||||
|
filters: {
|
||||||
|
course: props.courseName,
|
||||||
|
member: user.data?.name,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: ['enrollments', props.courseName, user.data?.name],
|
||||||
|
})
|
||||||
|
|
||||||
|
const chapter = createDocumentResource({
|
||||||
|
doctype: 'Course Chapter',
|
||||||
|
name: props.chapterName,
|
||||||
|
auto: true,
|
||||||
|
cache: ['chapter', props.chapterName],
|
||||||
|
onSuccess(data) {
|
||||||
|
progress.submit()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveProgress = () => {
|
||||||
|
call('lms.lms.doctype.course_lesson.course_lesson.save_progress', {
|
||||||
|
lesson: chapter.doc.lessons[0].lesson,
|
||||||
|
course: props.courseName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = createResource({
|
||||||
|
url: 'frappe.client.get_value',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'LMS Course Progress',
|
||||||
|
fieldname: 'status',
|
||||||
|
filters: {
|
||||||
|
member: user.data?.name,
|
||||||
|
lesson: chapter.doc.lessons[0].lesson,
|
||||||
|
chapter: chapter.doc.name,
|
||||||
|
course: chapter.doc?.course,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
readyToRender.value = true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const enrollStudent = () => {
|
||||||
|
enrollment.insert.submit(
|
||||||
|
{
|
||||||
|
course: props.courseName,
|
||||||
|
member: user.data?.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
window.location.reload()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Courses',
|
||||||
|
route: { name: 'Courses' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: chapter.doc?.course_title,
|
||||||
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: chapter.doc?.title,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageMeta = computed(() => {
|
||||||
|
return {
|
||||||
|
title: chapter?.doc?.title,
|
||||||
|
description: __('This is a chapter in the course {0}').format(
|
||||||
|
chapter?.doc?.course_title
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
updateDocumentTitle(pageMeta)
|
||||||
|
</script>
|
||||||
@@ -27,6 +27,12 @@ const routes = [
|
|||||||
component: () => import('@/pages/Lesson.vue'),
|
component: () => import('@/pages/Lesson.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/courses/:courseName/learn/:chapterName',
|
||||||
|
name: 'SCORMChapter',
|
||||||
|
component: () => import('@/pages/SCORMChapter.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/batches',
|
path: '/batches',
|
||||||
name: 'Batches',
|
name: 'Batches',
|
||||||
|
|||||||
10
frontend/src/stores/sidebar.js
Normal file
10
frontend/src/stores/sidebar.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useSidebar = defineStore('sidebar', () => {
|
||||||
|
const isSidebarCollapsed = ref(false)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSidebarCollapsed,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -93,7 +93,7 @@ export function showToast(title, text, icon, iconClasses = null) {
|
|||||||
if (!iconClasses) {
|
if (!iconClasses) {
|
||||||
if (icon == 'check') {
|
if (icon == 'check') {
|
||||||
iconClasses = 'bg-green-600 text-white rounded-md p-px'
|
iconClasses = 'bg-green-600 text-white rounded-md p-px'
|
||||||
} else if (icon == 'circle-warn') {
|
} else if (icon == 'alert-circle') {
|
||||||
iconClasses = 'bg-yellow-600 text-white rounded-md p-px'
|
iconClasses = 'bg-yellow-600 text-white rounded-md p-px'
|
||||||
} else {
|
} else {
|
||||||
iconClasses = 'bg-red-600 text-white rounded-md p-px'
|
iconClasses = 'bg-red-600 text-white rounded-md p-px'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
import zipfile
|
import zipfile
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from frappe.translate import get_all_translations
|
from frappe.translate import get_all_translations
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -883,38 +884,47 @@ def give_dicussions_permission():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_chapter(title, course, is_scorm_package, scorm_package):
|
def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None):
|
||||||
values = frappe._dict(
|
values = frappe._dict(
|
||||||
{"title": title, "course": course, "is_scorm_package": is_scorm_package}
|
{"title": title, "course": course, "is_scorm_package": is_scorm_package}
|
||||||
)
|
)
|
||||||
|
|
||||||
scorm_package = frappe._dict(scorm_package)
|
|
||||||
if is_scorm_package:
|
if is_scorm_package:
|
||||||
package = frappe.get_doc("File", scorm_package.name)
|
scorm_package = frappe._dict(scorm_package)
|
||||||
zip_path = package.get_full_path()
|
extract_path = extract_package(course, title, scorm_package)
|
||||||
|
|
||||||
# Extract the zip file
|
|
||||||
extract_path = frappe.get_site_path("public", "files", "scorm", course, title)
|
|
||||||
zipfile.ZipFile(zip_path).extractall(extract_path)
|
|
||||||
|
|
||||||
values.update(
|
values.update(
|
||||||
{
|
{
|
||||||
"scorm_package": scorm_package.name,
|
"scorm_package": scorm_package.name,
|
||||||
"scorm_package_path": extract_path,
|
"scorm_package_path": extract_path.split("public")[1],
|
||||||
"manifest_file": get_manifest_file(extract_path),
|
"manifest_file": get_manifest_file(extract_path).split("public")[1],
|
||||||
"launch_file": get_launch_file(extract_path),
|
"launch_file": get_launch_file(extract_path).split("public")[1],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
chapter = frappe.new_doc("Course Chapter")
|
if name:
|
||||||
print(values.title)
|
chapter = frappe.get_doc("Course Chapter", name)
|
||||||
|
else:
|
||||||
|
chapter = frappe.new_doc("Course Chapter")
|
||||||
|
|
||||||
chapter.update(values)
|
chapter.update(values)
|
||||||
print(chapter.title)
|
chapter.save()
|
||||||
chapter.insert()
|
|
||||||
|
if is_scorm_package and not len(chapter.lessons):
|
||||||
|
add_lesson(title, chapter.name, course)
|
||||||
|
|
||||||
return chapter
|
return chapter
|
||||||
|
|
||||||
|
|
||||||
|
def extract_package(course, title, scorm_package):
|
||||||
|
package = frappe.get_doc("File", scorm_package.name)
|
||||||
|
zip_path = package.get_full_path()
|
||||||
|
|
||||||
|
extract_path = frappe.get_site_path("public", "files", "scorm", course, title)
|
||||||
|
zipfile.ZipFile(zip_path).extractall(extract_path)
|
||||||
|
return extract_path
|
||||||
|
|
||||||
|
|
||||||
def get_manifest_file(extract_path):
|
def get_manifest_file(extract_path):
|
||||||
manifest_file = None
|
manifest_file = None
|
||||||
for root, dirs, files in os.walk(extract_path):
|
for root, dirs, files in os.walk(extract_path):
|
||||||
@@ -930,16 +940,17 @@ def get_manifest_file(extract_path):
|
|||||||
def get_launch_file(extract_path):
|
def get_launch_file(extract_path):
|
||||||
launch_file = None
|
launch_file = None
|
||||||
manifest_file = get_manifest_file(extract_path)
|
manifest_file = get_manifest_file(extract_path)
|
||||||
print(extract_path)
|
|
||||||
|
|
||||||
if manifest_file:
|
if manifest_file:
|
||||||
with open(manifest_file) as file:
|
with open(manifest_file) as file:
|
||||||
data = file.read()
|
data = file.read()
|
||||||
print(data)
|
|
||||||
dom = parseString(data)
|
dom = parseString(data)
|
||||||
resource = dom.getElementsByTagName("resource")
|
resource = dom.getElementsByTagName("resource")
|
||||||
for res in resource:
|
for res in resource:
|
||||||
if res.getAttribute("adlcp:scormtype") == "sco":
|
if (
|
||||||
|
res.getAttribute("adlcp:scormtype") == "sco"
|
||||||
|
or res.getAttribute("adlcp:scormType") == "sco"
|
||||||
|
):
|
||||||
launch_file = res.getAttribute("href")
|
launch_file = res.getAttribute("href")
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -947,3 +958,47 @@ def get_launch_file(extract_path):
|
|||||||
launch_file = os.path.join(os.path.dirname(manifest_file), launch_file)
|
launch_file = os.path.join(os.path.dirname(manifest_file), launch_file)
|
||||||
|
|
||||||
return launch_file
|
return launch_file
|
||||||
|
|
||||||
|
|
||||||
|
def add_lesson(title, chapter, course):
|
||||||
|
lesson = frappe.new_doc("Course Lesson")
|
||||||
|
lesson.update(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"chapter": chapter,
|
||||||
|
"course": course,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lesson.insert()
|
||||||
|
|
||||||
|
lesson_reference = frappe.new_doc("Lesson Reference")
|
||||||
|
lesson_reference.update(
|
||||||
|
{
|
||||||
|
"lesson": lesson.name,
|
||||||
|
"parent": chapter,
|
||||||
|
"parenttype": "Course Chapter",
|
||||||
|
"parentfield": "lessons",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lesson_reference.insert()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def delete_chapter(chapter):
|
||||||
|
chapterInfo = frappe.db.get_value(
|
||||||
|
"Course Chapter", chapter, ["is_scorm_package", "scorm_package_path"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if chapterInfo.is_scorm_package:
|
||||||
|
delete_scorm_package(chapterInfo.scorm_package_path)
|
||||||
|
|
||||||
|
frappe.db.delete("Chapter Reference", {"chapter": chapter})
|
||||||
|
frappe.db.delete("Lesson Reference", {"parent": chapter})
|
||||||
|
frappe.db.delete("Course Lesson", {"chapter": chapter})
|
||||||
|
frappe.db.delete("Course Chapter", chapter)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_scorm_package(scorm_package_path):
|
||||||
|
scorm_package_path = frappe.get_site_path("public", scorm_package_path)
|
||||||
|
if os.path.exists(scorm_package_path):
|
||||||
|
shutil.rmtree(scorm_package_path)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"title",
|
"title",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"course",
|
"course",
|
||||||
|
"course_title",
|
||||||
"scorm_section",
|
"scorm_section",
|
||||||
"is_scorm_package",
|
"is_scorm_package",
|
||||||
"scorm_package",
|
"scorm_package",
|
||||||
@@ -93,6 +94,13 @@
|
|||||||
"fieldtype": "Code",
|
"fieldtype": "Code",
|
||||||
"label": "SCORM Package Path",
|
"label": "SCORM Package Path",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "course.title",
|
||||||
|
"fieldname": "course_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Course Title",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -103,7 +111,7 @@
|
|||||||
"link_fieldname": "chapter"
|
"link_fieldname": "chapter"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-11-11 16:25:45.586160",
|
"modified": "2024-11-15 12:03:31.370943",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Chapter",
|
"name": "Course Chapter",
|
||||||
@@ -123,17 +131,14 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 1,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "LMS Student",
|
"role": "LMS Student",
|
||||||
"select": 1,
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "title",
|
"search_fields": "title",
|
||||||
|
|||||||
@@ -8,12 +8,18 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"chapter",
|
|
||||||
"course",
|
|
||||||
"column_break_4",
|
|
||||||
"title",
|
"title",
|
||||||
"include_in_preview",
|
"include_in_preview",
|
||||||
"index_label",
|
"column_break_4",
|
||||||
|
"chapter",
|
||||||
|
"is_scorm_package",
|
||||||
|
"course",
|
||||||
|
"section_break_11",
|
||||||
|
"content",
|
||||||
|
"body",
|
||||||
|
"column_break_cjmf",
|
||||||
|
"instructor_content",
|
||||||
|
"instructor_notes",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"youtube",
|
"youtube",
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
@@ -22,13 +28,7 @@
|
|||||||
"question",
|
"question",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
"file_type",
|
"file_type",
|
||||||
"section_break_11",
|
"column_break_syza",
|
||||||
"content",
|
|
||||||
"body",
|
|
||||||
"column_break_cjmf",
|
|
||||||
"instructor_content",
|
|
||||||
"instructor_notes",
|
|
||||||
"help_section",
|
|
||||||
"help"
|
"help"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -59,12 +59,6 @@
|
|||||||
"label": "Title",
|
"label": "Title",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "index_label",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Index Label",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@@ -74,14 +68,7 @@
|
|||||||
"fieldname": "body",
|
"fieldname": "body",
|
||||||
"fieldtype": "Markdown Editor",
|
"fieldtype": "Markdown Editor",
|
||||||
"ignore_xss_filter": 1,
|
"ignore_xss_filter": 1,
|
||||||
"label": "Body",
|
"label": "Body"
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "help_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Help"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "help",
|
"fieldname": "help",
|
||||||
@@ -158,11 +145,23 @@
|
|||||||
"fieldname": "instructor_content",
|
"fieldname": "instructor_content",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Instructor Content"
|
"label": "Instructor Content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_syza",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "chapter.is_scorm_package",
|
||||||
|
"fieldname": "is_scorm_package",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is SCORM Package",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-10-08 11:04:54.748773",
|
"modified": "2024-11-14 13:46:56.838659",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Lesson",
|
"name": "Course Lesson",
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class CourseLesson(Document):
|
|||||||
ex.lesson = None
|
ex.lesson = None
|
||||||
ex.course = None
|
ex.course = None
|
||||||
ex.index_ = 0
|
ex.index_ = 0
|
||||||
ex.index_label = ""
|
|
||||||
ex.save(ignore_permissions=True)
|
ex.save(ignore_permissions=True)
|
||||||
|
|
||||||
def check_and_create_folder(self):
|
def check_and_create_folder(self):
|
||||||
|
|||||||
@@ -1128,11 +1128,20 @@ def get_course_outline(course, progress=False):
|
|||||||
chapter_details = frappe.db.get_value(
|
chapter_details = frappe.db.get_value(
|
||||||
"Course Chapter",
|
"Course Chapter",
|
||||||
chapter.chapter,
|
chapter.chapter,
|
||||||
["name", "title", "is_scorm_package", "launch_file"],
|
["name", "title", "is_scorm_package", "launch_file", "scorm_package"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
chapter_details["idx"] = chapter.idx
|
chapter_details["idx"] = chapter.idx
|
||||||
chapter_details.lessons = get_lessons(course, chapter_details, progress=progress)
|
chapter_details.lessons = get_lessons(course, chapter_details, progress=progress)
|
||||||
|
|
||||||
|
if chapter_details.is_scorm_package:
|
||||||
|
chapter_details.scorm_package = frappe.db.get_value(
|
||||||
|
"File",
|
||||||
|
chapter_details.scorm_package,
|
||||||
|
["file_name", "file_size", "file_url"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
outline.append(chapter_details)
|
outline.append(chapter_details)
|
||||||
return outline
|
return outline
|
||||||
|
|
||||||
@@ -1146,9 +1155,12 @@ def get_lesson(course, chapter, lesson):
|
|||||||
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
|
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
|
||||||
)
|
)
|
||||||
lesson_details = frappe.db.get_value(
|
lesson_details = frappe.db.get_value(
|
||||||
"Course Lesson", lesson_name, ["include_in_preview", "title"], as_dict=1
|
"Course Lesson",
|
||||||
|
lesson_name,
|
||||||
|
["include_in_preview", "title", "is_scorm_package"],
|
||||||
|
as_dict=1,
|
||||||
)
|
)
|
||||||
if not lesson_details:
|
if not lesson_details or lesson_details.is_scorm_package:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
membership = get_membership(course)
|
membership = get_membership(course)
|
||||||
|
|||||||
Reference in New Issue
Block a user