feat: record lesson progress
This commit is contained in:
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
||||
openMode: 0,
|
||||
},
|
||||
e2e: {
|
||||
baseUrl: "http://test:8000",
|
||||
baseUrl: "http://lms1:8000",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
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
|
||||
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">
|
||||
<SidebarLink
|
||||
v-for="link in sidebarLinks"
|
||||
:link="link"
|
||||
:isCollapsed="isSidebarCollapsed"
|
||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
class="mx-2 my-0.5"
|
||||
/>
|
||||
</div>
|
||||
@@ -22,11 +22,11 @@
|
||||
>
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<div
|
||||
v-if="!isSidebarCollapsed"
|
||||
v-if="!sidebarStore.isSidebarCollapsed"
|
||||
class="flex items-center text-sm text-gray-600 my-1"
|
||||
>
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
@@ -53,7 +53,7 @@
|
||||
<SidebarLink
|
||||
v-for="link in sidebarSettings.data.web_pages"
|
||||
:link="link"
|
||||
:isCollapsed="isSidebarCollapsed"
|
||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
class="mx-2 my-0.5"
|
||||
:showControls="isModerator ? true : false"
|
||||
@openModal="openPageModal"
|
||||
@@ -64,17 +64,19 @@
|
||||
</div>
|
||||
<SidebarLink
|
||||
:link="{
|
||||
label: isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||
}"
|
||||
:isCollapsed="isSidebarCollapsed"
|
||||
@click="isSidebarCollapsed = !isSidebarCollapsed"
|
||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
@click="toggleSidebar()"
|
||||
class="m-2"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
<CollapseSidebar
|
||||
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>
|
||||
</template>
|
||||
@@ -96,12 +98,14 @@ import { ref, onMounted, inject, watch } from 'vue'
|
||||
import { getSidebarLinks } from '../utils'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useSidebar } from '@/stores/sidebar'
|
||||
import { ChevronRight, Plus } from 'lucide-vue-next'
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import PageModal from '@/components/Modals/PageModal.vue'
|
||||
|
||||
const { user, sidebarSettings } = sessionStore()
|
||||
const { userResource } = usersStore()
|
||||
let sidebarStore = useSidebar()
|
||||
const socket = inject('$socket')
|
||||
const unreadCount = ref(0)
|
||||
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>
|
||||
|
||||
@@ -140,7 +140,7 @@ function enrollStudent() {
|
||||
showToast(
|
||||
__('Please Login'),
|
||||
__('You need to login first to enroll for this course'),
|
||||
'circle-warn'
|
||||
'alert-circle'
|
||||
)
|
||||
setTimeout(() => {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'shadow rounded-md pt-2 px-2': showOutline && outline.data?.length,
|
||||
'shadow rounded-md py-2 px-2': showOutline && outline.data?.length,
|
||||
}"
|
||||
>
|
||||
<Disclosure
|
||||
@@ -25,21 +25,42 @@
|
||||
:key="chapter.name"
|
||||
:defaultOpen="openChapterDetail(chapter.idx)"
|
||||
>
|
||||
<DisclosureButton ref="" class="flex w-full p-2">
|
||||
<DisclosureButton ref="" class="flex items-center w-full p-2 group">
|
||||
<ChevronRight
|
||||
:class="{
|
||||
'rotate-90 transform duration-200': open,
|
||||
'duration-200': !open,
|
||||
hidden: chapter.is_scorm_package,
|
||||
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 }}
|
||||
</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>
|
||||
<DisclosurePanel>
|
||||
<DisclosurePanel v-if="!chapter.is_scorm_package">
|
||||
<Draggable
|
||||
v-if="!chapter.is_scorm_package"
|
||||
:list="chapter.lessons"
|
||||
:disabled="!allowEdit"
|
||||
item-key="name"
|
||||
@@ -103,9 +124,6 @@
|
||||
{{ __('Add Lesson') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button class="ml-2" @click="openChapterModal(chapter)">
|
||||
{{ __('Edit Chapter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
@@ -119,24 +137,26 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||
import { getCurrentInstance, inject, ref } from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||
import {
|
||||
ChevronRight,
|
||||
MonitorPlay,
|
||||
HelpCircle,
|
||||
FileText,
|
||||
Check,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
FilePenLine,
|
||||
HelpCircle,
|
||||
MonitorPlay,
|
||||
Trash2,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const route = useRoute()
|
||||
const expandAll = ref(true)
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
const showChapterModal = ref(false)
|
||||
const currentChapter = ref(null)
|
||||
const app = getCurrentInstance()
|
||||
@@ -206,8 +226,10 @@ const updateLessonIndex = createResource({
|
||||
|
||||
const trashLesson = (lessonName, chapterName) => {
|
||||
$dialog({
|
||||
title: __('Delete Lesson'),
|
||||
message: __('Are you sure you want to delete this lesson?'),
|
||||
title: __('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: [
|
||||
{
|
||||
label: __('Delete'),
|
||||
@@ -246,6 +268,61 @@ const updateOutline = (e) => {
|
||||
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>
|
||||
<style>
|
||||
.outline-lesson:has(.router-link-active) {
|
||||
|
||||
@@ -15,8 +15,13 @@
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="space-y-4">
|
||||
<FormControl ref="chapterInput" label="Title" v-model="chapter.title" />
|
||||
<div class="space-y-4 text-base">
|
||||
<FormControl
|
||||
ref="chapterInput"
|
||||
label="Title"
|
||||
v-model="chapter.title"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
:label="__('Is SCORM Package')"
|
||||
v-model="chapter.is_scorm_package"
|
||||
@@ -97,25 +102,14 @@ const chapter = reactive({
|
||||
})
|
||||
|
||||
const chapterResource = createResource({
|
||||
url: 'lms.lms.api.add_chapter',
|
||||
url: 'lms.lms.api.upsert_chapter',
|
||||
makeParams(values) {
|
||||
return {
|
||||
title: chapter.title,
|
||||
course: props.course,
|
||||
is_scorm_package: chapter.is_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,
|
||||
fieldname: 'title',
|
||||
value: chapter.title,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -140,12 +134,7 @@ const addChapter = async (close) => {
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
if (!chapter.title) {
|
||||
return __('Title is required')
|
||||
}
|
||||
if (chapter.is_scorm_package && !chapter.scorm_package) {
|
||||
return __('Please upload a SCORM package')
|
||||
}
|
||||
return validateChapter()
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
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 = () => {
|
||||
chapter.title = ''
|
||||
chapter.is_scorm_package = 0
|
||||
@@ -182,7 +180,7 @@ const cleanChapter = () => {
|
||||
}
|
||||
|
||||
const editChapter = (close) => {
|
||||
chapterEditResource.submit(
|
||||
chapterResource.submit(
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
@@ -206,6 +204,8 @@ watch(
|
||||
() => props.chapterDetail,
|
||||
(newChapter) => {
|
||||
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"
|
||||
></div>
|
||||
<div class="mt-10">
|
||||
<CourseOutline :courseName="course.data.name" :showOutline="true" />
|
||||
<CourseOutline
|
||||
:title="__('Course Outline')"
|
||||
:courseName="course.data.name"
|
||||
:showOutline="true"
|
||||
/>
|
||||
</div>
|
||||
<CourseReviews
|
||||
:courseName="course.data.name"
|
||||
|
||||
@@ -244,7 +244,10 @@ const lesson = createResource({
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
if (Object.keys(data).length === 0) {
|
||||
router.push({ name: 'Courses' })
|
||||
router.push({
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: props.courseName },
|
||||
})
|
||||
return
|
||||
}
|
||||
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'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/courses/:courseName/learn/:chapterName',
|
||||
name: 'SCORMChapter',
|
||||
component: () => import('@/pages/SCORMChapter.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/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 (icon == 'check') {
|
||||
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'
|
||||
} else {
|
||||
iconClasses = 'bg-red-600 text-white rounded-md p-px'
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import frappe
|
||||
import zipfile
|
||||
import os
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
from frappe.translate import get_all_translations
|
||||
from frappe import _
|
||||
@@ -883,38 +884,47 @@ def give_dicussions_permission():
|
||||
|
||||
|
||||
@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(
|
||||
{"title": title, "course": course, "is_scorm_package": is_scorm_package}
|
||||
)
|
||||
|
||||
scorm_package = frappe._dict(scorm_package)
|
||||
if is_scorm_package:
|
||||
package = frappe.get_doc("File", scorm_package.name)
|
||||
zip_path = package.get_full_path()
|
||||
|
||||
# Extract the zip file
|
||||
extract_path = frappe.get_site_path("public", "files", "scorm", course, title)
|
||||
zipfile.ZipFile(zip_path).extractall(extract_path)
|
||||
scorm_package = frappe._dict(scorm_package)
|
||||
extract_path = extract_package(course, title, scorm_package)
|
||||
|
||||
values.update(
|
||||
{
|
||||
"scorm_package": scorm_package.name,
|
||||
"scorm_package_path": extract_path,
|
||||
"manifest_file": get_manifest_file(extract_path),
|
||||
"launch_file": get_launch_file(extract_path),
|
||||
"scorm_package_path": extract_path.split("public")[1],
|
||||
"manifest_file": get_manifest_file(extract_path).split("public")[1],
|
||||
"launch_file": get_launch_file(extract_path).split("public")[1],
|
||||
}
|
||||
)
|
||||
|
||||
chapter = frappe.new_doc("Course Chapter")
|
||||
print(values.title)
|
||||
if name:
|
||||
chapter = frappe.get_doc("Course Chapter", name)
|
||||
else:
|
||||
chapter = frappe.new_doc("Course Chapter")
|
||||
|
||||
chapter.update(values)
|
||||
print(chapter.title)
|
||||
chapter.insert()
|
||||
chapter.save()
|
||||
|
||||
if is_scorm_package and not len(chapter.lessons):
|
||||
add_lesson(title, chapter.name, course)
|
||||
|
||||
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):
|
||||
manifest_file = None
|
||||
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):
|
||||
launch_file = None
|
||||
manifest_file = get_manifest_file(extract_path)
|
||||
print(extract_path)
|
||||
|
||||
if manifest_file:
|
||||
with open(manifest_file) as file:
|
||||
data = file.read()
|
||||
print(data)
|
||||
dom = parseString(data)
|
||||
resource = dom.getElementsByTagName("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")
|
||||
break
|
||||
|
||||
@@ -947,3 +958,47 @@ def get_launch_file(extract_path):
|
||||
launch_file = os.path.join(os.path.dirname(manifest_file), 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",
|
||||
"column_break_3",
|
||||
"course",
|
||||
"course_title",
|
||||
"scorm_section",
|
||||
"is_scorm_package",
|
||||
"scorm_package",
|
||||
@@ -93,6 +94,13 @@
|
||||
"fieldtype": "Code",
|
||||
"label": "SCORM Package Path",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "course.title",
|
||||
"fieldname": "course_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Course Title",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -103,7 +111,7 @@
|
||||
"link_fieldname": "chapter"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-11 16:25:45.586160",
|
||||
"modified": "2024-11-15 12:03:31.370943",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Chapter",
|
||||
@@ -123,17 +131,14 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "title",
|
||||
|
||||
@@ -8,12 +8,18 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"chapter",
|
||||
"course",
|
||||
"column_break_4",
|
||||
"title",
|
||||
"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",
|
||||
"youtube",
|
||||
"column_break_9",
|
||||
@@ -22,13 +28,7 @@
|
||||
"question",
|
||||
"column_break_15",
|
||||
"file_type",
|
||||
"section_break_11",
|
||||
"content",
|
||||
"body",
|
||||
"column_break_cjmf",
|
||||
"instructor_content",
|
||||
"instructor_notes",
|
||||
"help_section",
|
||||
"column_break_syza",
|
||||
"help"
|
||||
],
|
||||
"fields": [
|
||||
@@ -59,12 +59,6 @@
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "index_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Index Label",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -74,14 +68,7 @@
|
||||
"fieldname": "body",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Body",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "help_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Help"
|
||||
"label": "Body"
|
||||
},
|
||||
{
|
||||
"fieldname": "help",
|
||||
@@ -158,11 +145,23 @@
|
||||
"fieldname": "instructor_content",
|
||||
"fieldtype": "Text",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-10-08 11:04:54.748773",
|
||||
"modified": "2024-11-14 13:46:56.838659",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Lesson",
|
||||
|
||||
@@ -52,7 +52,6 @@ class CourseLesson(Document):
|
||||
ex.lesson = None
|
||||
ex.course = None
|
||||
ex.index_ = 0
|
||||
ex.index_label = ""
|
||||
ex.save(ignore_permissions=True)
|
||||
|
||||
def check_and_create_folder(self):
|
||||
|
||||
@@ -1128,11 +1128,20 @@ def get_course_outline(course, progress=False):
|
||||
chapter_details = frappe.db.get_value(
|
||||
"Course Chapter",
|
||||
chapter.chapter,
|
||||
["name", "title", "is_scorm_package", "launch_file"],
|
||||
["name", "title", "is_scorm_package", "launch_file", "scorm_package"],
|
||||
as_dict=True,
|
||||
)
|
||||
chapter_details["idx"] = chapter.idx
|
||||
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)
|
||||
return outline
|
||||
|
||||
@@ -1146,9 +1155,12 @@ def get_lesson(course, chapter, lesson):
|
||||
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
|
||||
)
|
||||
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 {}
|
||||
|
||||
membership = get_membership(course)
|
||||
|
||||
Reference in New Issue
Block a user