Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af533a7a2c | ||
|
|
acbede157f | ||
|
|
f63a627ff2 | ||
|
|
b1a0556c12 | ||
|
|
0097ede6ed | ||
|
|
b72774e54d | ||
|
|
3027a9e523 | ||
|
|
c3995952b3 | ||
|
|
ff1642382c | ||
|
|
cfe35e40da | ||
|
|
c3238a9f91 | ||
|
|
58f08bf065 | ||
|
|
d3ac6ea337 | ||
|
|
6649b7955f | ||
|
|
15a53d33e0 | ||
|
|
57f09542a2 | ||
|
|
fa384b391d | ||
|
|
12b138c39f | ||
|
|
420a5f39eb | ||
|
|
12c2666bd1 | ||
|
|
1ecbc2e3f9 | ||
|
|
dcf5c72cad | ||
|
|
2ebf6be609 | ||
|
|
4ce7019ce6 | ||
|
|
3faf814162 | ||
|
|
52bd9825d8 | ||
|
|
b6028e741c | ||
|
|
cbc7892b25 | ||
|
|
a4fa2ef0b3 | ||
|
|
96de90cb5f | ||
|
|
dfb22c81c3 | ||
|
|
6a70ed18d8 | ||
|
|
629c237349 | ||
|
|
cf014bca3c | ||
|
|
9323d8e17d | ||
|
|
61e13aa7cd | ||
|
|
acb8c6c500 | ||
|
|
f504841a5c | ||
|
|
fb3d8e4f7d | ||
|
|
be49ba6d04 | ||
|
|
24ffed11fb | ||
|
|
73754bd104 | ||
|
|
0c6029cbe8 | ||
|
|
a643e9ae83 | ||
|
|
08ac3948c3 | ||
|
|
78d289b9c0 | ||
|
|
3473bdb527 | ||
|
|
a7f8835222 | ||
|
|
d6441955fc | ||
|
|
67d265e864 | ||
|
|
17031f1df0 | ||
|
|
234a24baa2 | ||
|
|
9a58f4688b | ||
|
|
87c1c928ba | ||
|
|
493b8297ea | ||
|
|
4d16602190 | ||
|
|
89222b23c3 | ||
|
|
89a181c7d5 | ||
|
|
c0aecf30c1 | ||
|
|
fc8ef21802 | ||
|
|
2e1aac4931 | ||
|
|
c45da4313e | ||
|
|
3a1a843747 | ||
|
|
92c380c74b | ||
|
|
c51e7b0037 | ||
|
|
e25f161980 | ||
|
|
000d9dbcef | ||
|
|
0dcfd7e482 | ||
|
|
e933012a34 | ||
|
|
71db3ae6da | ||
|
|
c5f091fae8 | ||
|
|
4e61d569ac | ||
|
|
2d5c76e106 | ||
|
|
2e0abad61c | ||
|
|
3ea52a4e41 | ||
|
|
c05e253b8d | ||
|
|
08b2063e45 | ||
|
|
4a8c8185c2 | ||
|
|
74ed7b3160 | ||
|
|
38e6e4345f | ||
|
|
8004982e2e | ||
|
|
e6a532a870 | ||
|
|
f90465210e | ||
|
|
4b3a71e424 | ||
|
|
5499e7294d | ||
|
|
619262aa97 | ||
|
|
693d2942aa | ||
|
|
b4cf62920c | ||
|
|
03636d6930 | ||
|
|
7c1e1c86c7 | ||
|
|
8a5eceaf05 | ||
|
|
720425d1fb | ||
|
|
1f105b9ae5 | ||
|
|
d43442be5c | ||
|
|
3360b114b4 | ||
|
|
94835b4117 | ||
|
|
e6ed0b21e5 | ||
|
|
37db021682 | ||
|
|
6014a5ccce | ||
|
|
c07207b564 | ||
|
|
fe1f78f8aa | ||
|
|
1709c6b658 | ||
|
|
d3583a2cfb | ||
|
|
634035fbc0 | ||
|
|
3c5b18411b | ||
|
|
82bb45a9ef | ||
|
|
373f3df196 | ||
|
|
6021f15bac | ||
|
|
da71fb2c23 | ||
|
|
8f6f35d7c1 | ||
|
|
7aa5f4d20b | ||
|
|
64b54b05a6 | ||
|
|
22b1f22df4 | ||
|
|
ae4e5539d7 | ||
|
|
dbd96329b5 | ||
|
|
c118ec7c4a | ||
|
|
7aab449502 | ||
|
|
cf166b3a57 | ||
|
|
da5910d40d | ||
|
|
8640ecf9be | ||
|
|
c4faceff30 | ||
|
|
01bd017bda | ||
|
|
d76357981b | ||
|
|
19b759e9fb | ||
|
|
df3bca6405 | ||
|
|
5cde79b5eb | ||
|
|
9b35cdbddc | ||
|
|
70ec22004a | ||
|
|
95ed77421a | ||
|
|
d64ec9817c | ||
|
|
ce01b7634f | ||
|
|
e0819f83bc | ||
|
|
f87d28c2f5 | ||
|
|
544b59744b | ||
|
|
467dfb831d | ||
|
|
4c4b4eaf55 | ||
|
|
227e5d00e5 | ||
|
|
73e9e384c8 | ||
|
|
5bebdcba68 | ||
|
|
1c2e52ae4b | ||
|
|
9377e89561 | ||
|
|
4cae05ecbe | ||
|
|
909dcfd51e | ||
|
|
2bd96a1f2a | ||
|
|
aca41080ee | ||
|
|
1c351696a9 | ||
|
|
51a8958aa6 | ||
|
|
777b8aed02 | ||
|
|
3672b90075 | ||
|
|
92c7e613db | ||
|
|
5c58b85a00 | ||
|
|
8af82daa37 | ||
|
|
224bb18d3e | ||
|
|
aab7bdcc20 | ||
|
|
c5ca428d98 | ||
|
|
a085050d27 | ||
|
|
2442f35f56 | ||
|
|
53285a0d19 |
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://test_site_ui:8000",
|
baseUrl: "http://lms1:8000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe("Course Creation", () => {
|
|||||||
cy.visit("/lms/courses");
|
cy.visit("/lms/courses");
|
||||||
|
|
||||||
// Create a course
|
// Create a course
|
||||||
cy.get("a").contains("New").click();
|
cy.get("header").children().last().children().last().click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.url().should("include", "/courses/new/edit");
|
cy.url().should("include", "/courses/new/edit");
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ describe("Course Creation", () => {
|
|||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("label").contains("Title").type("Test Chapter");
|
cy.get("label").contains("Title").type("Test Chapter");
|
||||||
cy.button("Add Chapter").click();
|
cy.button("Create").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add Lesson
|
// Add Lesson
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"codemirror-editor-vue3": "^2.8.0",
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.69",
|
"frappe-ui": "^0.1.72",
|
||||||
"lucide-vue-next": "^0.383.0",
|
"lucide-vue-next": "^0.383.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createListResource, Avatar } from 'frappe-ui'
|
import { createResource, Avatar } from 'frappe-ui'
|
||||||
import { timeAgo } from '@/utils'
|
import { timeAgo } from '@/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -35,24 +35,15 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const communications = createListResource({
|
const communications = createResource({
|
||||||
doctype: 'Communication',
|
url: 'lms.lms.api.get_announcements',
|
||||||
fields: [
|
makeParams(value) {
|
||||||
'subject',
|
return {
|
||||||
'content',
|
batch: props.batch,
|
||||||
'recipients',
|
}
|
||||||
'cc',
|
|
||||||
'communication_date',
|
|
||||||
'sender',
|
|
||||||
'sender_full_name',
|
|
||||||
],
|
|
||||||
filters: {
|
|
||||||
reference_doctype: 'LMS Batch',
|
|
||||||
reference_name: props.batch,
|
|
||||||
},
|
},
|
||||||
orderBy: 'communication_date desc',
|
|
||||||
auto: true,
|
auto: true,
|
||||||
cache: ['batch', props.batch],
|
cache: ['announcement', props.batch],
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -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,7 @@ watch(userResource, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let isSidebarCollapsed = ref(getSidebarFromStorage())
|
const toggleSidebar = () => {
|
||||||
|
sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<label class="block" :class="labelClasses" v-if="attrs.label">
|
<label class="block" :class="labelClasses" v-if="attrs.label">
|
||||||
{{ attrs.label }}
|
{{ attrs.label }}
|
||||||
|
<span class="text-red-500" v-if="attrs.required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref="autocomplete"
|
ref="autocomplete"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block mb-1" :class="labelClasses" v-if="label">
|
<label class="block mb-1" :class="labelClasses" v-if="label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
<span class="text-red-500" v-if="required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="grid grid-cols-3 gap-1">
|
<div class="grid grid-cols-3 gap-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -115,6 +116,9 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: (value) => `${value} is an Invalid value`,
|
default: (value) => `${value} is an Invalid value`,
|
||||||
},
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const values = defineModel()
|
const values = defineModel()
|
||||||
@@ -152,24 +156,11 @@ const filterOptions = createResource({
|
|||||||
url: 'frappe.desk.search.search_link',
|
url: 'frappe.desk.search.search_link',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
cache: [text.value, props.doctype],
|
cache: [text.value, props.doctype],
|
||||||
|
auto: true,
|
||||||
params: {
|
params: {
|
||||||
txt: text.value,
|
txt: text.value,
|
||||||
doctype: props.doctype,
|
doctype: props.doctype,
|
||||||
},
|
},
|
||||||
/* transform: (data) => {
|
|
||||||
let allData = data
|
|
||||||
.filter((c) => {
|
|
||||||
return c.description.split(', ')[1]
|
|
||||||
})
|
|
||||||
.map((option) => {
|
|
||||||
let email = option.description.split(', ')[1]
|
|
||||||
return {
|
|
||||||
label: option.label || email,
|
|
||||||
value: email,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return allData
|
|
||||||
}, */
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = computed(() => {
|
const options = computed(() => {
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center flex-wrap space-y-1 space-x-1 relative top-4 px-2 w-fit"
|
class="flex items-center flex-wrap space-x-1 relative top-4 px-2 w-fit"
|
||||||
>
|
>
|
||||||
<Badge v-if="course.featured" variant="subtle" theme="green" size="md">
|
<Badge v-if="course.featured" variant="subtle" theme="green" size="md">
|
||||||
{{ __('Featured') }}
|
{{ __('Featured') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="subtle"
|
||||||
theme="gray"
|
theme="gray"
|
||||||
size="md"
|
size="md"
|
||||||
v-for="tag in course.tags"
|
v-for="tag in course.tags"
|
||||||
@@ -30,29 +30,29 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-auto p-4">
|
<div class="flex flex-col flex-auto p-4">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div v-if="course.lesson_count">
|
<div v-if="course.lessons">
|
||||||
<Tooltip :text="__('Lessons')">
|
<Tooltip :text="__('Lessons')">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
||||||
{{ course.lesson_count }}
|
{{ course.lessons }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="course.enrollment_count">
|
<div v-if="course.enrollments">
|
||||||
<Tooltip :text="__('Enrolled Students')">
|
<Tooltip :text="__('Enrolled Students')">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<Users class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
<Users class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
||||||
{{ course.enrollment_count }}
|
{{ course.enrollments }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="course.avg_rating">
|
<div v-if="course.rating">
|
||||||
<Tooltip :text="__('Average Rating')">
|
<Tooltip :text="__('Average Rating')">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<Star class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
<Star class="h-4 w-4 stroke-1.5 text-gray-700 mr-1" />
|
||||||
{{ course.avg_rating }}
|
{{ course.rating }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,21 +93,19 @@
|
|||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<BookOpen class="h-5 w-5 stroke-1.5 text-gray-600" />
|
<BookOpen class="h-5 w-5 stroke-1.5 text-gray-600" />
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
{{ course.data.lesson_count }} {{ __('Lessons') }}
|
{{ course.data.lessons }} {{ __('Lessons') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<Users class="h-5 w-5 stroke-1.5 text-gray-600" />
|
<Users class="h-5 w-5 stroke-1.5 text-gray-600" />
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
{{ course.data.enrollment_count_formatted }}
|
{{ formatAmount(course.data.enrollments) }}
|
||||||
{{ __('Enrolled Students') }}
|
{{ __('Enrolled Students') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Star class="h-5 w-5 stroke-1.5 fill-orange-500 text-gray-50" />
|
<Star class="h-5 w-5 stroke-1.5 fill-orange-500 text-gray-50" />
|
||||||
<span class="ml-2">
|
<span class="ml-2"> {{ course.data.rating }} {{ __('Rating') }} </span>
|
||||||
{{ course.data.avg_rating }} {{ __('Rating') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +114,7 @@
|
|||||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource } from 'frappe-ui'
|
||||||
import { showToast } from '@/utils/'
|
import { showToast, formatAmount } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -142,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}`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="instructors.length == 1">
|
<span v-if="instructors?.length == 1">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
{{ instructors[0].full_name }}
|
{{ instructors[0].full_name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="instructors.length == 2">
|
<span v-if="instructors?.length == 2">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
{{ instructors[1].first_name }}
|
{{ instructors[1].first_name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="instructors.length > 2">
|
<span v-if="instructors?.length > 2">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
>
|
>
|
||||||
{{ instructors[0].first_name }}
|
{{ instructors[0].first_name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
and {{ instructors.length - 1 }} others
|
and {{ instructors?.length - 1 }} others
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -89,6 +110,7 @@
|
|||||||
</Draggable>
|
</Draggable>
|
||||||
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
|
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="!chapter.is_scorm_package"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'LessonForm',
|
name: 'LessonForm',
|
||||||
params: {
|
params: {
|
||||||
@@ -102,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>
|
||||||
@@ -118,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()
|
||||||
@@ -205,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'),
|
||||||
@@ -245,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) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
avg_rating: {
|
avg_rating: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
allowfullscreen
|
allowfullscreen
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="block in content.split('\n\n')">
|
<div v-for="block in content?.split('\n\n')">
|
||||||
<div v-if="block.includes('{{ YouTubeVideo')">
|
<div v-if="block.includes('{{ YouTubeVideo')">
|
||||||
<iframe
|
<iframe
|
||||||
class="youtube-video"
|
class="youtube-video"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div
|
||||||
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
class="flex text-sm font-medium space-x-2 cursor-pointer"
|
||||||
@click="openHelpDialog('upload')"
|
@click="openHelpDialog('upload')"
|
||||||
>
|
>
|
||||||
<span class="leading-5">
|
<span class="leading-5">
|
||||||
@@ -56,6 +56,21 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center text-sm font-medium space-x-2">
|
||||||
|
<span>
|
||||||
|
{{ __('What does include in preview mean?') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 mb-1 leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'If Include in Preview is enabled for a lesson then the lesson will also be accessible to non logged in users.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ExplanationVideos v-model="showExplanation" :type="type" />
|
<ExplanationVideos v-model="showExplanation" :type="type" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 text-gray-900 mt-auto">
|
<div class="flex items-center space-x-2 text-gray-900 mt-auto">
|
||||||
<a
|
<a
|
||||||
|
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
||||||
:href="cls.start_url"
|
:href="cls.start_url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="w-1/2 cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-gray-800 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 focus-visible:ring focus-visible:ring-gray-400 h-7 text-base px-2 rounded"
|
class="w-1/2 cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-gray-800 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 focus-visible:ring focus-visible:ring-gray-400 h-7 text-base px-2 rounded"
|
||||||
@@ -45,9 +46,10 @@
|
|||||||
{{ __('Start') }}
|
{{ __('Start') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
v-if="cls.date <= dayjs().format('YYYY-MM-DD')"
|
||||||
:href="cls.join_url"
|
:href="cls.join_url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="w-1/2 cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-gray-800 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 focus-visible:ring focus-visible:ring-gray-400 h-7 text-base px-2 rounded"
|
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-gray-800 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 focus-visible:ring focus-visible:ring-gray-400 h-7 text-base px-2 rounded"
|
||||||
>
|
>
|
||||||
<Video class="h-4 w-4 stroke-1.5" />
|
<Video class="h-4 w-4 stroke-1.5" />
|
||||||
{{ __('Join') }}
|
{{ __('Join') }}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
<div class="mb-1.5 text-sm text-gray-600">
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
{{ __('Subject') }}
|
{{ __('Subject') }}
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
</div>
|
</div>
|
||||||
<Input type="text" v-model="announcement.subject" />
|
<Input type="text" v-model="announcement.subject" />
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
|
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { createToast } from '@/utils/'
|
import { showToast } from '@/utils/'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
|
|
||||||
@@ -94,22 +95,14 @@ const makeAnnouncement = (close) => {
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
close()
|
close()
|
||||||
createToast({
|
showToast(
|
||||||
title: 'Success',
|
__('Success'),
|
||||||
text: 'Announcement has been sent successfully',
|
__('Announcement has been sent successfully'),
|
||||||
icon: 'Check',
|
'check'
|
||||||
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
)
|
||||||
})
|
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
createToast({
|
showToast(__('Error'), __(err.messages?.[0] || err), 'check')
|
||||||
title: 'Error',
|
|
||||||
text: err.messages?.[0] || err,
|
|
||||||
icon: 'x',
|
|
||||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
|
||||||
position: 'top-center',
|
|
||||||
timeout: 10,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,12 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<Link doctype="LMS Course" v-model="course" :label="__('Course')" />
|
<Link
|
||||||
|
doctype="LMS Course"
|
||||||
|
v-model="course"
|
||||||
|
:label="__('Course')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
<Link
|
<Link
|
||||||
doctype="Course Evaluator"
|
doctype="Course Evaluator"
|
||||||
v-model="evaluator"
|
v-model="evaluator"
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:options="{
|
:options="{
|
||||||
title: __('Add Chapter'),
|
title: chapterDetail ? __('Edit Chapter') : __('Add Chapter'),
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: chapterDetail ? __('Edit Chapter') : __('Add Chapter'),
|
label: chapterDetail ? __('Edit') : __('Create'),
|
||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
onClick: (close) =>
|
onClick: (close) =>
|
||||||
chapterDetail ? editChapter(close) : addChapter(close),
|
chapterDetail ? editChapter(close) : addChapter(close),
|
||||||
@@ -15,24 +15,69 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<FormControl
|
<div class="space-y-4 text-base">
|
||||||
ref="chapterInput"
|
<FormControl label="Title" v-model="chapter.title" :required="true" />
|
||||||
label="Title"
|
<FormControl
|
||||||
v-model="chapter.title"
|
:label="__('Is SCORM Package')"
|
||||||
class="mb-4"
|
v-model="chapter.is_scorm_package"
|
||||||
/>
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div v-if="chapter.is_scorm_package">
|
||||||
|
<FileUploader
|
||||||
|
v-if="!chapter.scorm_package"
|
||||||
|
:fileTypes="['.zip']"
|
||||||
|
:validateFile="validateFile"
|
||||||
|
@success="(file) => (chapter.scorm_package = file)"
|
||||||
|
>
|
||||||
|
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
||||||
|
<div class="mb-4">
|
||||||
|
<Button @click="openFileSelector" :loading="uploading">
|
||||||
|
{{
|
||||||
|
uploading ? `Uploading ${progress}%` : 'Upload an zip file'
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
|
<div v-else class="">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="border rounded-md p-2 mr-2">
|
||||||
|
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>
|
||||||
|
{{ chapter.scorm_package.file_name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ getFileSize(chapter.scorm_package.file_size) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<X
|
||||||
|
@click="() => (chapter.scorm_package = null)"
|
||||||
|
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FormControl, createResource } from 'frappe-ui'
|
import {
|
||||||
|
Button,
|
||||||
|
createResource,
|
||||||
|
Dialog,
|
||||||
|
FileUploader,
|
||||||
|
FormControl,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch, ref } from 'vue'
|
import { defineModel, reactive, watch, ref } from 'vue'
|
||||||
import { createToast } from '@/utils/'
|
import { showToast, getFileSize } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const outline = defineModel('outline')
|
const outline = defineModel('outline')
|
||||||
const chapterInput = ref(null)
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
course: {
|
course: {
|
||||||
@@ -46,30 +91,19 @@ const props = defineProps({
|
|||||||
|
|
||||||
const chapter = reactive({
|
const chapter = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
|
is_scorm_package: 0,
|
||||||
|
scorm_package: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const chapterResource = createResource({
|
const chapterResource = createResource({
|
||||||
url: 'frappe.client.insert',
|
url: 'lms.lms.api.upsert_chapter',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doc: {
|
title: chapter.title,
|
||||||
doctype: 'Course Chapter',
|
course: props.course,
|
||||||
title: chapter.title,
|
is_scorm_package: chapter.is_scorm_package,
|
||||||
description: chapter.description,
|
scorm_package: chapter.scorm_package,
|
||||||
course: props.course,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -89,14 +123,12 @@ const chapterReference = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const addChapter = (close) => {
|
const addChapter = async (close) => {
|
||||||
chapterResource.submit(
|
chapterResource.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
if (!chapter.title) {
|
return validateChapter()
|
||||||
return 'Title is required'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
capture('chapter_created')
|
capture('chapter_created')
|
||||||
@@ -104,30 +136,45 @@ const addChapter = (close) => {
|
|||||||
{ name: data.name },
|
{ name: data.name },
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
chapter.title = ''
|
cleanChapter()
|
||||||
outline.value.reload()
|
outline.value.reload()
|
||||||
createToast({
|
showToast(
|
||||||
text: 'Chapter added successfully',
|
__('Success'),
|
||||||
icon: 'check',
|
__('Chapter added successfully'),
|
||||||
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
'check'
|
||||||
})
|
)
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showError(err)
|
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showError(err)
|
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
chapter.scorm_package = null
|
||||||
|
}
|
||||||
|
|
||||||
const editChapter = (close) => {
|
const editChapter = (close) => {
|
||||||
chapterEditResource.submit(
|
chapterResource.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
@@ -137,43 +184,29 @@ const editChapter = (close) => {
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
outline.value.reload()
|
outline.value.reload()
|
||||||
createToast({
|
showToast(__('Success'), __('Chapter updated successfully'), 'check')
|
||||||
text: 'Chapter updated successfully',
|
|
||||||
icon: 'check',
|
|
||||||
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
|
||||||
})
|
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showError(err)
|
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showError = (err) => {
|
|
||||||
createToast({
|
|
||||||
title: 'Error',
|
|
||||||
text: err.messages?.[0] || err,
|
|
||||||
icon: 'x',
|
|
||||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
|
||||||
position: 'top-center',
|
|
||||||
timeout: 10,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(show, () => {
|
const validateFile = (file) => {
|
||||||
if (show.value) {
|
let extension = file.name.split('.').pop().toLowerCase()
|
||||||
setTimeout(() => {
|
if (extension !== 'zip') {
|
||||||
chapterInput.value.$el.querySelector('input').focus()
|
return __('Only zip files are allowed')
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -69,7 +69,18 @@
|
|||||||
:label="__('Headline')"
|
:label="__('Headline')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<FormControl type="textarea" v-model="profile.bio" :label="__('Bio')" />
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
|
{{ __('Bio') }}
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
:fixedMenu="true"
|
||||||
|
@change="(val) => (profile.bio = val)"
|
||||||
|
:content="profile.bio"
|
||||||
|
editorClass="prose-sm py-2 px-2 min-h-[200px] border-gray-300 hover:border-gray-400 rounded-md bg-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -81,6 +92,7 @@ import {
|
|||||||
FileUploader,
|
FileUploader,
|
||||||
Button,
|
Button,
|
||||||
createResource,
|
createResource,
|
||||||
|
TextEditor,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, watch, defineModel } from 'vue'
|
import { reactive, watch, defineModel } from 'vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
|
|||||||
@@ -154,10 +154,12 @@ function submitEvaluation(close) {
|
|||||||
const getCourses = () => {
|
const getCourses = () => {
|
||||||
let courses = []
|
let courses = []
|
||||||
for (const course of props.courses) {
|
for (const course of props.courses) {
|
||||||
courses.push({
|
if (course.evaluator) {
|
||||||
label: course.title,
|
courses.push({
|
||||||
value: course.course,
|
label: course.title,
|
||||||
})
|
value: course.course,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return courses
|
return courses
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
v-model="liveClass.title"
|
v-model="liveClass.title"
|
||||||
:label="__('Title')"
|
:label="__('Title')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
:text="
|
:text="
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
type="time"
|
type="time"
|
||||||
:label="__('Time')"
|
:label="__('Time')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
type="select"
|
type="select"
|
||||||
:options="getTimezoneOptions()"
|
:options="getTimezoneOptions()"
|
||||||
:label="__('Timezone')"
|
:label="__('Timezone')"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:label="__('Date')"
|
:label="__('Date')"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<Tooltip :text="__('Duration of the live class in minutes')">
|
<Tooltip :text="__('Duration of the live class in minutes')">
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -57,6 +61,7 @@
|
|||||||
v-model="liveClass.duration"
|
v-model="liveClass.duration"
|
||||||
:label="__('Duration')"
|
:label="__('Duration')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -156,25 +161,34 @@ const submitLiveClass = (close) => {
|
|||||||
return createLiveClass.submit(liveClass, {
|
return createLiveClass.submit(liveClass, {
|
||||||
validate() {
|
validate() {
|
||||||
if (!liveClass.title) {
|
if (!liveClass.title) {
|
||||||
return 'Please enter a title.'
|
return __('Please enter a title.')
|
||||||
}
|
}
|
||||||
if (!liveClass.date) {
|
if (!liveClass.date) {
|
||||||
return 'Please select a date.'
|
return __('Please select a date.')
|
||||||
}
|
|
||||||
if (dayjs(liveClass.date).isSameOrBefore(dayjs(), 'day')) {
|
|
||||||
return 'Please select a future date.'
|
|
||||||
}
|
}
|
||||||
if (!liveClass.time) {
|
if (!liveClass.time) {
|
||||||
return 'Please select a time.'
|
return __('Please select a time.')
|
||||||
}
|
|
||||||
if (!valideTime()) {
|
|
||||||
return 'Please enter a valid time in the format HH:mm.'
|
|
||||||
}
|
|
||||||
if (!liveClass.duration) {
|
|
||||||
return 'Please select a duration.'
|
|
||||||
}
|
}
|
||||||
if (!liveClass.timezone) {
|
if (!liveClass.timezone) {
|
||||||
return 'Please select a timezone.'
|
return __('Please select a timezone.')
|
||||||
|
}
|
||||||
|
if (!valideTime()) {
|
||||||
|
return __('Please enter a valid time in the format HH:mm.')
|
||||||
|
}
|
||||||
|
const liveClassDateTime = dayjs(`${liveClass.date}T${liveClass.time}`).tz(
|
||||||
|
liveClass.timezone,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
liveClassDateTime.isSameOrBefore(
|
||||||
|
dayjs().tz(liveClass.timezone, false),
|
||||||
|
'minute'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return __('Please select a future date and time.')
|
||||||
|
}
|
||||||
|
if (!liveClass.duration) {
|
||||||
|
return __('Please select a duration.')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
id="existing"
|
id="existing"
|
||||||
value="existing"
|
value="existing"
|
||||||
v-model="questionType"
|
v-model="questionType"
|
||||||
class="w-3 h-3 accent-gray-900"
|
class="w-3 h-3 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<label for="existing">
|
<label for="existing" class="cursor-pointer">
|
||||||
{{ __('Add an existing question') }}
|
{{ __('Add an existing question') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
id="new"
|
id="new"
|
||||||
value="new"
|
value="new"
|
||||||
v-model="questionType"
|
v-model="questionType"
|
||||||
class="w-3 h-3"
|
class="w-3 h-3 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<label for="new">
|
<label for="new" class="cursor-pointer">
|
||||||
{{ __('Create a new question') }}
|
{{ __('Create a new question') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +127,7 @@ const populateFields = () => {
|
|||||||
let counter = 1
|
let counter = 1
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
while (counter <= 4) {
|
while (counter <= 4) {
|
||||||
question[`${field}_${counter}`] = field === 'is_correct' ? false : ''
|
question[`${field}_${counter}`] = field === 'is_correct' ? false : null
|
||||||
counter++
|
counter++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="quiz.data">
|
<div v-if="quiz.data">
|
||||||
<div
|
<div
|
||||||
class="bg-blue-100 space-y-1 py-2 px-2 rounded-md text-sm text-blue-800"
|
class="bg-blue-100 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-blue-800"
|
||||||
>
|
>
|
||||||
<div class="leading-5">
|
<div class="leading-5">
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center w-full duration-300 ease-in-out group"
|
class="flex items-center w-full duration-300 ease-in-out group"
|
||||||
:class="isCollapsed ? 'p-1' : 'px-2 py-1'"
|
:class="isCollapsed ? 'p-1 relative' : 'px-2 py-1'"
|
||||||
>
|
>
|
||||||
<Tooltip :text="link.label" placement="right">
|
<Tooltip :text="link.label" placement="right">
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
@@ -29,7 +29,15 @@
|
|||||||
>
|
>
|
||||||
{{ __(link.label) }}
|
{{ __(link.label) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="link.count" class="!ml-auto block text-xs text-gray-600">
|
<span
|
||||||
|
v-if="link.count"
|
||||||
|
class="!ml-auto block text-xs text-gray-600"
|
||||||
|
:class="
|
||||||
|
isCollapsed && link.count > 9
|
||||||
|
? 'absolute top-[2px] right-0 bg-white'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
{{ link.count }}
|
{{ link.count }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@timeupdate="updateTime"
|
@timeupdate="updateTime"
|
||||||
@ended="videoEnded"
|
@ended="videoEnded"
|
||||||
@click="togglePlay"
|
@click="togglePlay"
|
||||||
|
oncontextmenu="return false"
|
||||||
class="rounded-lg border border-gray-100 group cursor-pointer"
|
class="rounded-lg border border-gray-100 group cursor-pointer"
|
||||||
ref="videoRef"
|
ref="videoRef"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
</header>
|
</header>
|
||||||
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-screen">
|
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-screen">
|
||||||
<div class="border-r-2">
|
<div class="border-r-2">
|
||||||
<Tabs v-model="tabIndex" :tabs="tabs" tablistClass="overflow-y-hidden">
|
<Tabs
|
||||||
|
v-model="tabIndex"
|
||||||
|
:tabs="tabs"
|
||||||
|
tablistClass="overflow-y-hidden sticky top-11 bg-white z-10"
|
||||||
|
>
|
||||||
<template #tab="{ tab, selected }" class="overflow-x-hidden">
|
<template #tab="{ tab, selected }" class="overflow-x-hidden">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
@@ -236,7 +240,7 @@ const breadcrumbs = computed(() => {
|
|||||||
const isStudent = computed(() => {
|
const isStudent = computed(() => {
|
||||||
return (
|
return (
|
||||||
user?.data &&
|
user?.data &&
|
||||||
batch.data?.students.length &&
|
batch.data?.students?.length &&
|
||||||
batch.data?.students.includes(user.data.name)
|
batch.data?.students.includes(user.data.name)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<FormControl v-model="batch.title" :label="__('Title')" />
|
<FormControl
|
||||||
|
v-model="batch.title"
|
||||||
|
:label="__('Title')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -32,61 +36,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div>
|
<div class="text-xs text-gray-600 mb-2">
|
||||||
<FileUploader
|
{{ __('Meta Image') }}
|
||||||
v-if="!batch.image"
|
</div>
|
||||||
class="mt-4"
|
<FileUploader
|
||||||
:fileTypes="['image/*']"
|
v-if="!batch.image"
|
||||||
:validateFile="validateFile"
|
:fileTypes="['image/*']"
|
||||||
@success="(file) => saveImage(file)"
|
:validateFile="validateFile"
|
||||||
>
|
@success="(file) => saveImage(file)"
|
||||||
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
>
|
||||||
<div class="mb-4">
|
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
|
||||||
{{ uploading ? `Uploading ${progress}%` : 'Upload an image' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="mb-4">
|
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{{ __('Meta Image') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="border rounded-md p-2 mr-2">
|
<div class="border rounded-md w-fit py-5 px-20">
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
<Image class="size-5 stroke-1 text-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="ml-4">
|
||||||
<span>
|
<Button @click="openFileSelector">
|
||||||
{{ batch.image.file_name }}
|
{{ __('Upload') }}
|
||||||
</span>
|
</Button>
|
||||||
<span class="text-sm text-gray-500 mt-1">
|
<div class="mt-2 text-gray-600 text-sm">
|
||||||
{{ getFileSize(batch.image.file_size) }}
|
{{
|
||||||
</span>
|
__(
|
||||||
|
'Appears when the batch URL is shared on any online platform'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
|
<div v-else class="mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img :src="batch.image.file_url" class="border rounded-md w-40" />
|
||||||
|
<div class="ml-4">
|
||||||
|
<Button @click="removeImage()">
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</Button>
|
||||||
|
<div class="mt-2 text-gray-600 text-sm">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Appears when the batch URL is shared on any online platform'
|
||||||
|
)
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<X
|
|
||||||
@click="removeImage()"
|
|
||||||
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MultiSelect
|
|
||||||
v-model="instructors"
|
|
||||||
doctype="User"
|
|
||||||
:label="__('Instructors')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<MultiSelect
|
||||||
|
v-model="instructors"
|
||||||
|
doctype="User"
|
||||||
|
:label="__('Instructors')"
|
||||||
|
:required="true"
|
||||||
|
:filters="{ ignore_user_type: 1 }"
|
||||||
|
/>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.description"
|
v-model="batch.description"
|
||||||
:label="__('Description')"
|
:label="__('Description')"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
class="my-4"
|
class="my-4"
|
||||||
|
:placeholder="__('Short description of the batch')"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-600 mb-1">
|
<label class="block text-sm text-gray-600 mb-1">
|
||||||
{{ __('Batch Details') }}
|
{{ __('Batch Details') }}
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
:content="batch.batch_details"
|
:content="batch.batch_details"
|
||||||
@@ -108,12 +124,14 @@
|
|||||||
:label="__('Start Date')"
|
:label="__('Start Date')"
|
||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.end_date"
|
v-model="batch.end_date"
|
||||||
:label="__('End Date')"
|
:label="__('End Date')"
|
||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -122,18 +140,22 @@
|
|||||||
:label="__('Start Time')"
|
:label="__('Start Time')"
|
||||||
type="time"
|
type="time"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.end_time"
|
v-model="batch.end_time"
|
||||||
:label="__('End Time')"
|
:label="__('End Time')"
|
||||||
type="time"
|
type="time"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.timezone"
|
v-model="batch.timezone"
|
||||||
:label="__('Timezone')"
|
:label="__('Timezone')"
|
||||||
type="text"
|
type="text"
|
||||||
|
:placeholder="__('Example: IST (+5:30)')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,6 +171,7 @@
|
|||||||
:label="__('Seat Count')"
|
:label="__('Seat Count')"
|
||||||
type="number"
|
type="number"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:placeholder="__('Number of seats available')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.evaluation_end_date"
|
v-model="batch.evaluation_end_date"
|
||||||
@@ -228,11 +251,11 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { getFileSize, showToast } from '../utils'
|
import { showToast } from '../utils'
|
||||||
import { X, FileText } from 'lucide-vue-next'
|
import { Image } from 'lucide-vue-next'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
{{ __('Loading Batches...') }}
|
{{ __('Loading Batches...') }}
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
v-if="hasBatches"
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
:tabs="makeTabs"
|
:tabs="makeTabs"
|
||||||
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
||||||
@@ -79,24 +80,63 @@
|
|||||||
<BatchCard :batch="batch" />
|
<BatchCard :batch="batch" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="p-5 italic text-gray-500">
|
||||||
v-else
|
{{ __('No {0} batches').format(tab.label.toLowerCase()) }}
|
||||||
class="grid flex-1 place-items-center text-xl font-medium text-gray-500"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col items-center justify-center mt-4">
|
|
||||||
<div>
|
|
||||||
{{ __('No {0} batches found').format(tab.label.toLowerCase()) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
!batches.loading &&
|
||||||
|
!hasBatches &&
|
||||||
|
(user.data?.is_instructor || user.data?.is_moderator)
|
||||||
|
"
|
||||||
|
class="grid grid-cols-3 p-5"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'BatchForm',
|
||||||
|
params: {
|
||||||
|
batchName: 'new',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="bg-gray-50 py-32 px-5 rounded-md">
|
||||||
|
<div class="flex flex-col items-center text-center space-y-2">
|
||||||
|
<Plus
|
||||||
|
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
|
||||||
|
/>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ __('Create a Batch') }}
|
||||||
|
</div>
|
||||||
|
<span class="text-gray-700 text-sm leading-4">
|
||||||
|
{{ __('You can link courses and assessments to it.') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!batches.loading && !hasBatches"
|
||||||
|
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||||
|
>
|
||||||
|
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
|
||||||
|
<div class="text-xl font-medium">
|
||||||
|
{{ __('No batches found') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'There are no batches available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
createListResource,
|
|
||||||
createResource,
|
createResource,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
@@ -104,13 +144,14 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Select,
|
Select,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||||
import BatchCard from '@/components/BatchCard.vue'
|
import BatchCard from '@/components/BatchCard.vue'
|
||||||
import { inject, ref, computed, onMounted, watch } from 'vue'
|
import { inject, ref, computed, onMounted, watch } from 'vue'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
|
const hasBatches = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
@@ -119,10 +160,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const batches = createListResource({
|
const batches = createResource({
|
||||||
doctype: 'LMS Batch',
|
doctype: 'LMS Batch',
|
||||||
url: 'lms.lms.utils.get_batches',
|
url: 'lms.lms.utils.get_batches',
|
||||||
cache: ['batches', user?.data?.email],
|
cache: ['batches', user.data?.email],
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -183,6 +224,14 @@ const addToTabs = (label) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(batches, () => {
|
||||||
|
Object.keys(batches.data).forEach((key) => {
|
||||||
|
if (batches.data[key].length) {
|
||||||
|
hasBatches.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentCategory.value,
|
() => currentCategory.value,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -16,16 +16,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="course.data.avg_rating"
|
v-if="course.data.rating"
|
||||||
:text="__('Average Rating')"
|
:text="__('Average Rating')"
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
>
|
>
|
||||||
<Star class="h-5 w-5 text-gray-100 fill-orange-500" />
|
<Star class="h-5 w-5 text-gray-100 fill-orange-500" />
|
||||||
<span class="ml-1">
|
<span class="ml-1">
|
||||||
{{ course.data.avg_rating }}
|
{{ course.data.rating }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span v-if="course.data.avg_rating" class="mx-3">·</span>
|
<span v-if="course.data.rating" class="mx-3">·</span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="course.data.enrollment_count"
|
v-if="course.data.enrollment_count"
|
||||||
:text="__('Enrolled Students')"
|
:text="__('Enrolled Students')"
|
||||||
@@ -67,14 +67,18 @@
|
|||||||
<CourseCardOverlay :course="course" class="md:hidden mb-4" />
|
<CourseCardOverlay :course="course" class="md:hidden mb-4" />
|
||||||
<div
|
<div
|
||||||
v-html="course.data.description"
|
v-html="course.data.description"
|
||||||
class="course-description"
|
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"
|
||||||
:avg_rating="course.data.avg_rating"
|
:avg_rating="course.data.rating"
|
||||||
:membership="course.data.membership"
|
:membership="course.data.membership"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +120,7 @@ const breadcrumbs = computed(() => {
|
|||||||
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
||||||
items.push({
|
items.push({
|
||||||
label: course?.data?.title,
|
label: course?.data?.title,
|
||||||
route: { name: 'CourseDetail', params: { course: course?.data?.name } },
|
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
@@ -131,26 +135,6 @@ const pageMeta = computed(() => {
|
|||||||
updateDocumentTitle(pageMeta)
|
updateDocumentTitle(pageMeta)
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.course-description p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
.course-description li {
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-description ol {
|
|
||||||
list-style: auto;
|
|
||||||
margin: revert;
|
|
||||||
padding: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-description ul {
|
|
||||||
list-style: disc;
|
|
||||||
margin: revert;
|
|
||||||
padding: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-group {
|
.avatar-group {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
>
|
>
|
||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
<div class="flex items-center mt-3 md:mt-0">
|
<div class="flex items-center mt-3 md:mt-0">
|
||||||
|
<Button v-if="courseResource.data?.name" @click="trashCourse()">
|
||||||
|
<template #prefix>
|
||||||
|
<Trash2 class="w-4 h-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
<span>
|
||||||
|
{{ __('Delete') }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
<Button variant="solid" @click="submitCourse()" class="ml-2">
|
<Button variant="solid" @click="submitCourse()" class="ml-2">
|
||||||
<span>
|
<span>
|
||||||
{{ __('Save') }}
|
{{ __('Save') }}
|
||||||
@@ -23,15 +31,23 @@
|
|||||||
v-model="course.title"
|
v-model="course.title"
|
||||||
:label="__('Title')"
|
:label="__('Title')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.short_introduction"
|
v-model="course.short_introduction"
|
||||||
:label="__('Short Introduction')"
|
:label="__('Short Introduction')"
|
||||||
|
:placeholder="
|
||||||
|
__(
|
||||||
|
'A one line introduction to the course that appears on the course card'
|
||||||
|
)
|
||||||
|
"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="mb-1.5 text-sm text-gray-700">
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
{{ __('Course Description') }}
|
{{ __('Course Description') }}
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
</div>
|
</div>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
:content="course.description"
|
:content="course.description"
|
||||||
@@ -41,49 +57,62 @@
|
|||||||
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FileUploader
|
<div class="mb-4">
|
||||||
v-if="!course.course_image"
|
<div class="text-xs text-gray-600 mb-2">
|
||||||
:fileTypes="['image/*']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => saveImage(file)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
|
||||||
>
|
|
||||||
<div class="mb-4">
|
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
|
||||||
{{
|
|
||||||
uploading ? `Uploading ${progress}%` : 'Upload an image'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="mb-4">
|
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{{ __('Course Image') }}
|
{{ __('Course Image') }}
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<FileUploader
|
||||||
<div class="border rounded-md p-2 mr-2">
|
v-if="!course.course_image"
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
:fileTypes="['image/*']"
|
||||||
|
:validateFile="validateFile"
|
||||||
|
@success="(file) => saveImage(file)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="border rounded-md w-fit py-5 px-20">
|
||||||
|
<Image class="size-5 stroke-1 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<Button @click="openFileSelector">
|
||||||
|
{{ __('Upload') }}
|
||||||
|
</Button>
|
||||||
|
<div class="mt-2 text-gray-600 text-sm">
|
||||||
|
{{
|
||||||
|
__('Appears on the course card in the course list')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FileUploader>
|
||||||
|
<div v-else class="mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img
|
||||||
|
:src="course.course_image.file_url"
|
||||||
|
class="border rounded-md w-40"
|
||||||
|
/>
|
||||||
|
<div class="ml-4">
|
||||||
|
<Button @click="removeImage()">
|
||||||
|
{{ __('Remove') }}
|
||||||
|
</Button>
|
||||||
|
<div class="mt-2 text-gray-600 text-sm">
|
||||||
|
{{ __('Appears on the course card in the course list') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
<span>
|
|
||||||
{{ course.course_image.file_name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500 mt-1">
|
|
||||||
{{ getFileSize(course.course_image.file_size) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<X
|
|
||||||
@click="removeImage()"
|
|
||||||
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="course.video_link"
|
v-model="course.video_link"
|
||||||
:label="__('Preview Video')"
|
:label="__('Preview Video')"
|
||||||
|
:placeholder="
|
||||||
|
__(
|
||||||
|
'Paste the youtube link of a short video introducing the course'
|
||||||
|
)
|
||||||
|
"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -104,6 +133,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="newTag"
|
v-model="newTag"
|
||||||
|
:placeholder="__('Keywords for the course')"
|
||||||
|
class="w-52"
|
||||||
@keyup.enter="updateTags()"
|
@keyup.enter="updateTags()"
|
||||||
id="tags"
|
id="tags"
|
||||||
/>
|
/>
|
||||||
@@ -121,6 +152,8 @@
|
|||||||
v-model="instructors"
|
v-model="instructors"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
:label="__('Instructors')"
|
:label="__('Instructors')"
|
||||||
|
:filters="{ ignore_user_type: 1 }"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="container border-t">
|
<div class="container border-t">
|
||||||
@@ -130,7 +163,7 @@
|
|||||||
<div class="grid grid-cols-3 gap-10 mb-4">
|
<div class="grid grid-cols-3 gap-10 mb-4">
|
||||||
<div
|
<div
|
||||||
v-if="user.data?.is_moderator"
|
v-if="user.data?.is_moderator"
|
||||||
class="flex flex-col space-y-3"
|
class="flex flex-col space-y-4"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -223,15 +256,11 @@ import {
|
|||||||
ref,
|
ref,
|
||||||
reactive,
|
reactive,
|
||||||
watch,
|
watch,
|
||||||
|
getCurrentInstance,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import {
|
import { showToast, updateDocumentTitle } from '@/utils'
|
||||||
convertToTitleCase,
|
|
||||||
showToast,
|
|
||||||
getFileSize,
|
|
||||||
updateDocumentTitle,
|
|
||||||
} from '@/utils'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { Image, Trash2, X } from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
@@ -243,6 +272,8 @@ const newTag = ref('')
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const instructors = ref([])
|
const instructors = ref([])
|
||||||
const settingsStore = useSettings()
|
const settingsStore = useSettings()
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
@@ -415,23 +446,37 @@ const submitCourse = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateMandatoryFields = () => {
|
const deleteCourse = createResource({
|
||||||
const mandatory_fields = [
|
url: 'lms.lms.api.delete_course',
|
||||||
'title',
|
makeParams(values) {
|
||||||
'short_introduction',
|
return {
|
||||||
'description',
|
course: props.courseName,
|
||||||
'video_link',
|
|
||||||
'course_image',
|
|
||||||
]
|
|
||||||
for (const field of mandatory_fields) {
|
|
||||||
if (!course[field]) {
|
|
||||||
let fieldLabel = convertToTitleCase(field.split('_').join(' '))
|
|
||||||
return `${fieldLabel} is mandatory`
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
if (course.paid_course && (!course.course_price || !course.currency)) {
|
onSuccess() {
|
||||||
return __('Course price and currency are mandatory for paid courses')
|
showToast(__('Success'), __('Course deleted successfully'), 'check')
|
||||||
}
|
router.push({ name: 'Courses' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const trashCourse = () => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Delete Course'),
|
||||||
|
message: __(
|
||||||
|
'Deleting the course will also delete all its chapters and lessons. Are you sure you want to delete this course?'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Delete'),
|
||||||
|
theme: 'red',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(close) {
|
||||||
|
deleteCourse.submit()
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
|
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
|
||||||
/>
|
/>
|
||||||
<div class="flex space-x-2 justify-end">
|
<div class="flex space-x-2 justify-end">
|
||||||
<div class="w-46 md:w-44">
|
<div class="w-40 md:w-44">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="categories.data?.length"
|
v-if="categories.data?.length"
|
||||||
type="select"
|
type="select"
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="user.data?.is_moderator || user.data?.is_instructor"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'CourseForm',
|
name: 'CourseForm',
|
||||||
params: {
|
params: {
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button v-if="user.data?.is_moderator" variant="solid">
|
<Button variant="solid">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="">
|
<div class="">
|
||||||
<Tabs
|
<Tabs
|
||||||
|
v-if="hasCourses"
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
||||||
:tabs="makeTabs"
|
:tabs="makeTabs"
|
||||||
@@ -101,18 +103,57 @@
|
|||||||
<CourseCard :course="course" />
|
<CourseCard :course="course" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="p-5 italic text-gray-500">
|
||||||
v-else
|
{{ __('No {0} courses').format(tab.label.toLowerCase()) }}
|
||||||
class="grid flex-1 place-items-center text-xl font-medium text-gray-500"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col items-center justify-center mt-4">
|
|
||||||
<div>
|
|
||||||
{{ __('No {0} courses found').format(tab.label.toLowerCase()) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
!courses.loading &&
|
||||||
|
(user.data?.is_moderator || user.data?.is_instructor)
|
||||||
|
"
|
||||||
|
class="grid grid-cols-3 p-5"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'CourseForm',
|
||||||
|
params: {
|
||||||
|
courseName: 'new',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="bg-gray-50 py-32 px-5 rounded-md">
|
||||||
|
<div class="flex flex-col items-center text-center space-y-2">
|
||||||
|
<Plus
|
||||||
|
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
|
||||||
|
/>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ __('Create a Course') }}
|
||||||
|
</div>
|
||||||
|
<span class="text-gray-700 text-sm leading-4">
|
||||||
|
{{ __('You can add chapters and lessons to it.') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!courses.loading && !hasCourses"
|
||||||
|
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||||
|
>
|
||||||
|
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
|
||||||
|
<div class="text-xl font-medium">
|
||||||
|
{{ __('No courses found') }}
|
||||||
|
</div>
|
||||||
|
<div class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'There are no courses available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -127,13 +168,14 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
import { Plus, Search } from 'lucide-vue-next'
|
import { BookOpen, Plus, Search } from 'lucide-vue-next'
|
||||||
import { ref, computed, inject, onMounted, watch } from 'vue'
|
import { ref, computed, inject, onMounted, watch } from 'vue'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
|
const hasCourses = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
@@ -223,6 +265,16 @@ const categories = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(courses, () => {
|
||||||
|
if (courses.data) {
|
||||||
|
Object.keys(courses.data).forEach((section) => {
|
||||||
|
if (courses.data[section].length) {
|
||||||
|
hasCourses.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentCategory.value,
|
() => currentCategory.value,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -7,7 +7,22 @@
|
|||||||
class="h-7"
|
class="h-7"
|
||||||
:items="[{ label: __('Jobs'), route: { name: 'Jobs' } }]"
|
:items="[{ label: __('Jobs'), route: { name: 'Jobs' } }]"
|
||||||
/>
|
/>
|
||||||
<div class="flex">
|
<div class="flex space-x-2">
|
||||||
|
<div class="w-40 md:w-44">
|
||||||
|
<FormControl
|
||||||
|
v-model="jobType"
|
||||||
|
type="select"
|
||||||
|
:options="jobTypes"
|
||||||
|
:placeholder="__('Type')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-28 md:w-36">
|
||||||
|
<FormControl type="text" placeholder="Search" v-model="searchQuery">
|
||||||
|
<template #prefix>
|
||||||
|
<Search class="w-4 h-4 stroke-1.5 text-gray-600" name="search" />
|
||||||
|
</template>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="user.data?.name"
|
v-if="user.data?.name"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -26,9 +41,9 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="jobs.data?.length">
|
<div v-if="jobsList?.length">
|
||||||
<div class="divide-y lg:w-3/4 mx-auto p-5">
|
<div class="divide-y lg:w-3/4 mx-auto p-5">
|
||||||
<div v-for="job in jobs.data">
|
<div v-for="job in jobsList">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'JobDetail',
|
name: 'JobDetail',
|
||||||
@@ -47,13 +62,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Breadcrumbs, createResource } from 'frappe-ui'
|
import { Button, Breadcrumbs, createResource, FormControl } from 'frappe-ui'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { Plus, Search } from 'lucide-vue-next'
|
||||||
import { inject, computed } from 'vue'
|
import { inject, computed, ref, onMounted } from 'vue'
|
||||||
import JobCard from '@/components/JobCard.vue'
|
import JobCard from '@/components/JobCard.vue'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const jobType = ref(null)
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
let queries = new URLSearchParams(location.search)
|
||||||
|
if (queries.has('type')) {
|
||||||
|
jobType.value = queries.get('type')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const jobs = createResource({
|
const jobs = createResource({
|
||||||
url: 'lms.lms.api.get_job_opportunities',
|
url: 'lms.lms.api.get_job_opportunities',
|
||||||
@@ -68,5 +92,32 @@ const pageMeta = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const jobsList = computed(() => {
|
||||||
|
let jobData = jobs.data
|
||||||
|
if (jobType.value && jobType.value != '') {
|
||||||
|
jobData = jobData.filter((job) => job.type == jobType.value)
|
||||||
|
}
|
||||||
|
if (searchQuery.value) {
|
||||||
|
let query = searchQuery.value.toLowerCase()
|
||||||
|
jobData = jobData.filter(
|
||||||
|
(job) =>
|
||||||
|
job.job_title.toLowerCase().includes(query) ||
|
||||||
|
job.company_name.toLowerCase().includes(query) ||
|
||||||
|
job.location.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return jobData
|
||||||
|
})
|
||||||
|
|
||||||
|
const jobTypes = computed(() => {
|
||||||
|
return [
|
||||||
|
'',
|
||||||
|
{ label: __('Full Time'), value: 'Full Time' },
|
||||||
|
{ label: __('Part Time'), value: 'Part Time' },
|
||||||
|
{ label: __('Contract'), value: 'Contract' },
|
||||||
|
{ label: __('Freelance'), value: 'Freelance' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
updateDocumentTitle(pageMeta)
|
updateDocumentTitle(pageMeta)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,14 +17,9 @@
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<Button v-if="user.data" @click="enrollStudent()" variant="solid">
|
||||||
v-if="user.data"
|
{{ __('Start Learning') }}
|
||||||
:to="{ name: 'CourseDetail', params: { courseName: courseName } }"
|
</Button>
|
||||||
>
|
|
||||||
<Button variant="solid">
|
|
||||||
{{ __('Start Learning') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
<Button v-else @click="redirectToLogin()">
|
<Button v-else @click="redirectToLogin()">
|
||||||
{{ __('Login') }}
|
{{ __('Login') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -108,7 +103,7 @@
|
|||||||
<span
|
<span
|
||||||
class="h-6 mr-1"
|
class="h-6 mr-1"
|
||||||
:class="{
|
:class="{
|
||||||
'avatar-group overlap': lesson.data.instructors.length > 1,
|
'avatar-group overlap': lesson.data.instructors?.length > 1,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
@@ -116,7 +111,10 @@
|
|||||||
:user="instructor"
|
:user="instructor"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<CourseInstructors :instructors="lesson.data.instructors" />
|
<CourseInstructors
|
||||||
|
v-if="lesson.data?.instructors"
|
||||||
|
:instructors="lesson.data.instructors"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
@@ -151,6 +149,7 @@
|
|||||||
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 mt-5"
|
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 mt-5"
|
||||||
>
|
>
|
||||||
<LessonContent
|
<LessonContent
|
||||||
|
v-if="lesson.data?.body"
|
||||||
:content="lesson.data.body"
|
:content="lesson.data.body"
|
||||||
:youtube="lesson.data.youtube"
|
:youtube="lesson.data.youtube"
|
||||||
:quizId="lesson.data.quiz_id"
|
:quizId="lesson.data.quiz_id"
|
||||||
@@ -194,7 +193,7 @@ import { createResource, Breadcrumbs, Button } from 'frappe-ui'
|
|||||||
import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
import Discussions from '@/components/Discussions.vue'
|
import Discussions from '@/components/Discussions.vue'
|
||||||
import { getEditorTools, updateDocumentTitle } from '../utils'
|
import { getEditorTools, updateDocumentTitle } from '../utils'
|
||||||
@@ -204,6 +203,7 @@ import CourseInstructors from '@/components/CourseInstructors.vue'
|
|||||||
import ProgressBar from '@/components/ProgressBar.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const allowDiscussions = ref(false)
|
const allowDiscussions = ref(false)
|
||||||
const editor = ref(null)
|
const editor = ref(null)
|
||||||
@@ -243,6 +243,13 @@ const lesson = createResource({
|
|||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
router.push({
|
||||||
|
name: 'CourseDetail',
|
||||||
|
params: { courseName: props.courseName },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
lessonProgress.value = data.membership?.progress
|
lessonProgress.value = data.membership?.progress
|
||||||
if (data.content) editor.value = renderEditor('editor', data.content)
|
if (data.content) editor.value = renderEditor('editor', data.content)
|
||||||
if (
|
if (
|
||||||
@@ -279,7 +286,7 @@ const renderEditor = (holder, content) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const markProgress = () => {
|
const markProgress = () => {
|
||||||
if (user.data && !lesson.data?.progress) {
|
if (user.data && lesson.data && !lesson.data.progress) {
|
||||||
progress.submit()
|
progress.submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,14 +308,14 @@ const breadcrumbs = computed(() => {
|
|||||||
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
||||||
items.push({
|
items.push({
|
||||||
label: lesson?.data?.course_title,
|
label: lesson?.data?.course_title,
|
||||||
route: { name: 'CourseDetail', params: { course: props.courseName } },
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
})
|
})
|
||||||
items.push({
|
items.push({
|
||||||
label: lesson?.data?.title,
|
label: lesson?.data?.title,
|
||||||
route: {
|
route: {
|
||||||
name: 'Lesson',
|
name: 'Lesson',
|
||||||
params: {
|
params: {
|
||||||
course: props.courseName,
|
courseName: props.courseName,
|
||||||
chapterNumber: props.chapterNumber,
|
chapterNumber: props.chapterNumber,
|
||||||
lessonNumber: props.lessonNumber,
|
lessonNumber: props.lessonNumber,
|
||||||
},
|
},
|
||||||
@@ -369,16 +376,40 @@ const checkIfDiscussionsAllowed = () => {
|
|||||||
|
|
||||||
const allowEdit = () => {
|
const allowEdit = () => {
|
||||||
if (user.data?.is_moderator) return true
|
if (user.data?.is_moderator) return true
|
||||||
if (lesson.data?.instructors.includes(user.data?.name)) return true
|
if (lesson.data?.instructors?.includes(user.data?.name)) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowInstructorContent = () => {
|
const allowInstructorContent = () => {
|
||||||
if (user.data?.is_moderator) return true
|
if (user.data?.is_moderator) return true
|
||||||
if (lesson.data?.instructors.includes(user.data?.name)) return true
|
if (lesson.data?.instructors?.includes(user.data?.name)) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enrollment = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams() {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'LMS Enrollment',
|
||||||
|
course: props.courseName,
|
||||||
|
member: user.data?.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const enrollStudent = () => {
|
||||||
|
enrollment.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
window.location.reload()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const redirectToLogin = () => {
|
const redirectToLogin = () => {
|
||||||
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
|
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,22 @@
|
|||||||
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b overflow-hidden bg-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b overflow-hidden bg-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs class="text-ellipsis" :items="breadcrumbs" />
|
<Breadcrumbs class="text-ellipsis" :items="breadcrumbs" />
|
||||||
<Button variant="solid" @click="saveLesson()" class="mt-3 md:mt-0">
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
@click="saveLesson({ showSuccessMessage: true })"
|
||||||
|
class="mt-3 md:mt-0"
|
||||||
|
>
|
||||||
{{ __('Save') }}
|
{{ __('Save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<div class="py-5">
|
<div class="py-5">
|
||||||
<div class="w-5/6 mx-auto">
|
<div class="w-5/6 mx-auto">
|
||||||
<FormControl v-model="lesson.title" label="Title" class="mb-4" />
|
<FormControl
|
||||||
|
v-model="lesson.title"
|
||||||
|
label="Title"
|
||||||
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="lesson.include_in_preview"
|
v-model="lesson.include_in_preview"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -69,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Breadcrumbs, FormControl, createResource, Button } from 'frappe-ui'
|
import { Breadcrumbs, Button, createResource, FormControl } from 'frappe-ui'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
reactive,
|
reactive,
|
||||||
@@ -89,6 +98,7 @@ const instructorEditor = ref(null)
|
|||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const openInstructorEditor = ref(false)
|
const openInstructorEditor = ref(false)
|
||||||
let autoSaveInterval
|
let autoSaveInterval
|
||||||
|
let showSuccessMessage = false
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
@@ -112,6 +122,7 @@ onMounted(() => {
|
|||||||
capture('lesson_form_opened')
|
capture('lesson_form_opened')
|
||||||
editor.value = renderEditor('content')
|
editor.value = renderEditor('content')
|
||||||
instructorEditor.value = renderEditor('instructor-notes')
|
instructorEditor.value = renderEditor('instructor-notes')
|
||||||
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderEditor = (holder) => {
|
const renderEditor = (holder) => {
|
||||||
@@ -181,12 +192,24 @@ const addInstructorNotes = (data) => {
|
|||||||
|
|
||||||
const enableAutoSave = () => {
|
const enableAutoSave = () => {
|
||||||
autoSaveInterval = setInterval(() => {
|
autoSaveInterval = setInterval(() => {
|
||||||
saveLesson()
|
saveLesson({ showSuccessMessage: false })
|
||||||
}, 10000)
|
}, 10000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyboardShortcut = (e) => {
|
||||||
|
if (
|
||||||
|
e.key === 's' &&
|
||||||
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
!e.target.classList.contains('ProseMirror')
|
||||||
|
) {
|
||||||
|
saveLesson({ showSuccessMessage: true })
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearInterval(autoSaveInterval)
|
clearInterval(autoSaveInterval)
|
||||||
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
const newLessonResource = createResource({
|
const newLessonResource = createResource({
|
||||||
@@ -338,7 +361,11 @@ const convertToJSON = (lessonData) => {
|
|||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveLesson = () => {
|
const saveLesson = (e) => {
|
||||||
|
showSuccessMessage = false
|
||||||
|
if (typeof e != 'undefined' && e.showSuccessMessage) {
|
||||||
|
showSuccessMessage = true
|
||||||
|
}
|
||||||
editor.value.save().then((outputData) => {
|
editor.value.save().then((outputData) => {
|
||||||
lesson.content = JSON.stringify(outputData)
|
lesson.content = JSON.stringify(outputData)
|
||||||
instructorEditor.value.save().then((outputData) => {
|
instructorEditor.value.save().then((outputData) => {
|
||||||
@@ -387,6 +414,11 @@ const editCurrentLesson = () => {
|
|||||||
validate() {
|
validate() {
|
||||||
return validateLesson()
|
return validateLesson()
|
||||||
},
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showSuccessMessage
|
||||||
|
? showToast('Success', 'Lesson updated successfully', 'check')
|
||||||
|
: ''
|
||||||
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast('Error', err.message, 'x')
|
showToast('Error', err.message, 'x')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
v-slot="{ idx, column, item }"
|
v-slot="{ idx, column, item }"
|
||||||
v-for="row in quiz.questions"
|
v-for="row in quiz.questions"
|
||||||
@click="openQuestionModal(row)"
|
@click="openQuestionModal(row)"
|
||||||
|
class="cursor-pointer"
|
||||||
>
|
>
|
||||||
<ListRowItem :item="item">
|
<ListRowItem :item="item">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -47,6 +47,22 @@
|
|||||||
</ListRows>
|
</ListRows>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||||
|
>
|
||||||
|
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
|
||||||
|
<div class="text-xl font-medium">
|
||||||
|
{{ __('No quizzes found') }}
|
||||||
|
</div>
|
||||||
|
<div class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'You have not created any quizzes yet. To create a new quiz, click on the "New Quiz" button above.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
@@ -61,7 +77,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { computed, inject, onMounted } from 'vue'
|
import { computed, inject, onMounted } from 'vue'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|||||||
204
frontend/src/pages/SCORMChapter.vue
Normal file
204
frontend/src/pages/SCORMChapter.vue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<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(`GET: ${key}`)
|
||||||
|
return getDataFromLMS(key)
|
||||||
|
},
|
||||||
|
SetValue: (key, value) => {
|
||||||
|
console.log(`SET: ${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: () => '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -5,6 +5,8 @@ import updateLocale from 'dayjs/esm/plugin/updateLocale'
|
|||||||
import isToday from 'dayjs/esm/plugin/isToday'
|
import isToday from 'dayjs/esm/plugin/isToday'
|
||||||
import isSameOrBefore from 'dayjs/esm/plugin/isSameOrBefore'
|
import isSameOrBefore from 'dayjs/esm/plugin/isSameOrBefore'
|
||||||
import isSameOrAfter from 'dayjs/esm/plugin/isSameOrAfter'
|
import isSameOrAfter from 'dayjs/esm/plugin/isSameOrAfter'
|
||||||
|
import utc from 'dayjs/esm/plugin/utc'
|
||||||
|
import timezone from 'dayjs/esm/plugin/timezone'
|
||||||
|
|
||||||
dayjs.extend(updateLocale)
|
dayjs.extend(updateLocale)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@@ -12,5 +14,7 @@ dayjs.extend(localizedFormat)
|
|||||||
dayjs.extend(isToday)
|
dayjs.extend(isToday)
|
||||||
dayjs.extend(isSameOrBefore)
|
dayjs.extend(isSameOrBefore)
|
||||||
dayjs.extend(isSameOrAfter)
|
dayjs.extend(isSameOrAfter)
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
export default dayjs
|
export default dayjs
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ export function formatNumberIntoCurrency(number, currency) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a function that formats numbers in thousands to k
|
||||||
|
|
||||||
|
export function formatAmount(amount) {
|
||||||
|
if (amount > 999) {
|
||||||
|
return (amount / 1000).toFixed(1) + 'k'
|
||||||
|
}
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
|
||||||
export function convertToTitleCase(str) {
|
export function convertToTitleCase(str) {
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return ''
|
return ''
|
||||||
@@ -84,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'
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class Quiz {
|
|||||||
app.mount(this.wrapper)
|
app.mount(this.wrapper)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center mb-2'>
|
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-gray-50 mb-2'>
|
||||||
<span class="font-medium">
|
<span class="font-medium">
|
||||||
Quiz: ${quiz}
|
Quiz: ${quiz}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1224,10 +1224,10 @@ fraction.js@^4.3.7:
|
|||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
frappe-ui@^0.1.69:
|
frappe-ui@^0.1.72:
|
||||||
version "0.1.69"
|
version "0.1.72"
|
||||||
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.69.tgz#bfc6d19dff97d2666c36da63f5de62f819539406"
|
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.72.tgz#f5550056ddee7ad4341f2c1825d046404d221820"
|
||||||
integrity sha512-MKHYTcRvmccZwTYlIcmf4OCbJQH5eqKXsq3Cj2lbnmoWuuTh9m7T3AoRKEwOIlZ0mSGCH9yzaF2BINBXGpIJdQ==
|
integrity sha512-XWYKmCjw3ViD+/+tZMUiYqwHFlMGMsVuazOYiN5bKlE+aiheJsnHlOOUyQswYX1Y7jNxuC7gGpSLNg2ZpXA7hA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@headlessui/vue" "^1.7.14"
|
"@headlessui/vue" "^1.7.14"
|
||||||
"@popperjs/core" "^2.11.2"
|
"@popperjs/core" "^2.11.2"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "2.9.0"
|
__version__ = "2.12.0"
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ doc_events = {
|
|||||||
# ---------------
|
# ---------------
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
"hourly": [
|
"hourly": [
|
||||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals"
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||||
|
"lms.lms.api.update_course_statistics",
|
||||||
],
|
],
|
||||||
"daily": ["lms.job.doctype.job_opportunity.job_opportunity.update_job_openings"],
|
"daily": ["lms.job.doctype.job_opportunity.job_opportunity.update_job_openings"],
|
||||||
}
|
}
|
||||||
@@ -185,6 +186,7 @@ jinja = {
|
|||||||
"lms.lms.utils.get_lesson_url",
|
"lms.lms.utils.get_lesson_url",
|
||||||
"lms.page_renderers.get_profile_url",
|
"lms.page_renderers.get_profile_url",
|
||||||
"lms.overrides.user.get_palette",
|
"lms.overrides.user.get_palette",
|
||||||
|
"lms.lms.utils.is_instructor",
|
||||||
],
|
],
|
||||||
"filters": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
||||||
|
from lms.lms.api import give_dicussions_permission
|
||||||
|
|
||||||
|
|
||||||
def after_install():
|
def after_install():
|
||||||
add_pages_to_nav()
|
add_pages_to_nav()
|
||||||
create_batch_source()
|
create_batch_source()
|
||||||
|
give_dicussions_permission()
|
||||||
|
|
||||||
|
|
||||||
def after_sync():
|
def after_sync():
|
||||||
|
|||||||
248
lms/lms/api.py
248
lms/lms/api.py
@@ -1,13 +1,20 @@
|
|||||||
"""API methods for the LMS.
|
"""API methods for the LMS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import frappe
|
import frappe
|
||||||
|
import zipfile
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
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 _
|
||||||
from frappe.query_builder import DocType
|
from frappe.query_builder import DocType
|
||||||
from frappe.query_builder.functions import Count
|
from frappe.query_builder.functions import Count
|
||||||
from frappe.utils import time_diff, now_datetime, get_datetime
|
from frappe.utils import time_diff, now_datetime, get_datetime, flt
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||||
|
from xml.dom.minidom import parseString
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -294,7 +301,8 @@ def get_branding():
|
|||||||
|
|
||||||
for field in image_fields:
|
for field in image_fields:
|
||||||
if website_settings.get(field):
|
if website_settings.get(field):
|
||||||
website_settings.update({field: get_file_info(website_settings.get(field))})
|
file_info = get_file_info(website_settings.get(field))
|
||||||
|
website_settings.update({field: json.loads(json.dumps(file_info))})
|
||||||
else:
|
else:
|
||||||
website_settings.update({field: None})
|
website_settings.update({field: None})
|
||||||
|
|
||||||
@@ -489,7 +497,15 @@ def delete_sidebar_item(webpage):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def delete_lesson(lesson, chapter):
|
def delete_lesson(lesson, chapter):
|
||||||
frappe.db.delete("Lesson Reference", {"parent": chapter, "lesson": lesson})
|
# Delete Reference
|
||||||
|
chapter = frappe.get_doc("Course Chapter", chapter)
|
||||||
|
chapter.lessons = [row for row in chapter.lessons if row.lesson != lesson]
|
||||||
|
chapter.save()
|
||||||
|
|
||||||
|
# Delete progress
|
||||||
|
frappe.db.delete("LMS Course Progress", {"lesson": lesson})
|
||||||
|
|
||||||
|
# Delete Lesson
|
||||||
frappe.db.delete("Course Lesson", lesson)
|
frappe.db.delete("Course Lesson", lesson)
|
||||||
|
|
||||||
|
|
||||||
@@ -760,3 +776,229 @@ def get_payment_gateway_details(payment_gateway):
|
|||||||
"doctype": doctype,
|
"doctype": doctype,
|
||||||
"docname": docname,
|
"docname": docname,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_course_statistics():
|
||||||
|
courses = frappe.get_all("LMS Course", fields=["name"])
|
||||||
|
|
||||||
|
for course in courses:
|
||||||
|
lessons = get_lesson_count(course.name)
|
||||||
|
|
||||||
|
enrollments = frappe.db.count(
|
||||||
|
"LMS Enrollment", {"course": course.name, "member_type": "Student"}
|
||||||
|
)
|
||||||
|
|
||||||
|
avg_rating = get_average_rating(course.name) or 0
|
||||||
|
avg_rating = flt(avg_rating, frappe.get_system_settings("float_precision") or 3)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Course",
|
||||||
|
course.name,
|
||||||
|
{"lessons": lessons, "enrollments": enrollments, "rating": avg_rating},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_announcements(batch):
|
||||||
|
return frappe.get_all(
|
||||||
|
"Communication",
|
||||||
|
filters={
|
||||||
|
"reference_doctype": "LMS Batch",
|
||||||
|
"reference_name": batch,
|
||||||
|
},
|
||||||
|
fields=[
|
||||||
|
"subject",
|
||||||
|
"content",
|
||||||
|
"recipients",
|
||||||
|
"cc",
|
||||||
|
"communication_date",
|
||||||
|
"sender",
|
||||||
|
"sender_full_name",
|
||||||
|
],
|
||||||
|
order_by="communication_date desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def delete_course(course):
|
||||||
|
|
||||||
|
chapters = frappe.get_all("Course Chapter", {"course": course}, pluck="name")
|
||||||
|
|
||||||
|
chapter_references = frappe.get_all(
|
||||||
|
"Chapter Reference", {"parent": course}, pluck="name"
|
||||||
|
)
|
||||||
|
|
||||||
|
for chapter in chapters:
|
||||||
|
lessons = frappe.get_all("Course Lesson", {"chapter": chapter}, pluck="name")
|
||||||
|
|
||||||
|
lesson_references = frappe.get_all(
|
||||||
|
"Lesson Reference", {"parent": chapter}, pluck="name"
|
||||||
|
)
|
||||||
|
|
||||||
|
for lesson in lesson_references:
|
||||||
|
frappe.delete_doc("Lesson Reference", lesson)
|
||||||
|
|
||||||
|
for lesson in lessons:
|
||||||
|
frappe.db.delete("LMS Course Progress", {"lesson": lesson})
|
||||||
|
|
||||||
|
topics = frappe.get_all(
|
||||||
|
"Discussion Topic",
|
||||||
|
{"reference_doctype": "Course Lesson", "reference_docname": lesson},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
for topic in topics:
|
||||||
|
frappe.db.delete("Discussion Reply", {"topic": topic})
|
||||||
|
|
||||||
|
frappe.db.delete("Discussion Topic", topic)
|
||||||
|
|
||||||
|
frappe.delete_doc("Course Lesson", lesson)
|
||||||
|
|
||||||
|
for chapter in chapter_references:
|
||||||
|
frappe.delete_doc("Chapter Reference", chapter)
|
||||||
|
|
||||||
|
for chapter in chapters:
|
||||||
|
frappe.delete_doc("Course Chapter", chapter)
|
||||||
|
|
||||||
|
frappe.db.delete("LMS Enrollment", {"course": course})
|
||||||
|
frappe.delete_doc("LMS Course", course)
|
||||||
|
|
||||||
|
|
||||||
|
def give_dicussions_permission():
|
||||||
|
doctypes = ["Discussion Topic", "Discussion Reply"]
|
||||||
|
roles = ["LMS Student", "Course Creator", "Moderator", "Batch Evaluator"]
|
||||||
|
for doctype in doctypes:
|
||||||
|
for role in roles:
|
||||||
|
if not frappe.db.exists("Custom DocPerm", {"parent": doctype, "role": role}):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Custom DocPerm",
|
||||||
|
"parent": doctype,
|
||||||
|
"role": role,
|
||||||
|
"read": 1,
|
||||||
|
"write": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
}
|
||||||
|
).save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_scorm_package:
|
||||||
|
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.split("public")[1],
|
||||||
|
"manifest_file": get_manifest_file(extract_path).split("public")[1],
|
||||||
|
"launch_file": get_launch_file(extract_path).split("public")[1],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if name:
|
||||||
|
chapter = frappe.get_doc("Course Chapter", name)
|
||||||
|
else:
|
||||||
|
chapter = frappe.new_doc("Course Chapter")
|
||||||
|
|
||||||
|
chapter.update(values)
|
||||||
|
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):
|
||||||
|
for file in files:
|
||||||
|
if file == "imsmanifest.xml":
|
||||||
|
manifest_file = os.path.join(root, file)
|
||||||
|
break
|
||||||
|
if manifest_file:
|
||||||
|
break
|
||||||
|
return manifest_file
|
||||||
|
|
||||||
|
|
||||||
|
def get_launch_file(extract_path):
|
||||||
|
launch_file = None
|
||||||
|
manifest_file = get_manifest_file(extract_path)
|
||||||
|
|
||||||
|
if manifest_file:
|
||||||
|
with open(manifest_file) as file:
|
||||||
|
data = file.read()
|
||||||
|
dom = parseString(data)
|
||||||
|
resource = dom.getElementsByTagName("resource")
|
||||||
|
for res in resource:
|
||||||
|
if (
|
||||||
|
res.getAttribute("adlcp:scormtype") == "sco"
|
||||||
|
or res.getAttribute("adlcp:scormType") == "sco"
|
||||||
|
):
|
||||||
|
launch_file = res.getAttribute("href")
|
||||||
|
break
|
||||||
|
|
||||||
|
if launch_file:
|
||||||
|
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)
|
||||||
|
|||||||
@@ -7,17 +7,3 @@ from frappe.model.document import Document
|
|||||||
|
|
||||||
class BatchStudent(Document):
|
class BatchStudent(Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def enroll_batch(batch_name):
|
|
||||||
if frappe.db.exists(
|
|
||||||
"Batch Student", {"student": frappe.session.user, "parent": batch_name}
|
|
||||||
):
|
|
||||||
frappe.throw("You are already enrolled in this batch")
|
|
||||||
enrollment = frappe.new_doc("Batch Student")
|
|
||||||
enrollment.student = frappe.session.user
|
|
||||||
enrollment.parent = batch_name
|
|
||||||
enrollment.parentfield = "students"
|
|
||||||
enrollment.parenttype = "LMS Batch"
|
|
||||||
enrollment.save(ignore_permissions=True)
|
|
||||||
|
|||||||
@@ -8,10 +8,17 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"course",
|
|
||||||
"title",
|
"title",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"description",
|
"course",
|
||||||
|
"course_title",
|
||||||
|
"scorm_section",
|
||||||
|
"is_scorm_package",
|
||||||
|
"scorm_package",
|
||||||
|
"scorm_package_path",
|
||||||
|
"column_break_dlnw",
|
||||||
|
"manifest_file",
|
||||||
|
"launch_file",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"lessons"
|
"lessons"
|
||||||
],
|
],
|
||||||
@@ -35,11 +42,6 @@
|
|||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "description",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Description"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_5",
|
"fieldname": "section_break_5",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
@@ -49,6 +51,56 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Lessons",
|
"label": "Lessons",
|
||||||
"options": "Lesson Reference"
|
"options": "Lesson Reference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_scorm_package",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is SCORM Package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "is_scorm_package",
|
||||||
|
"fieldname": "manifest_file",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Manifest File",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "is_scorm_package",
|
||||||
|
"fieldname": "launch_file",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Launch File",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scorm_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "SCORM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scorm_package",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "SCORM Package",
|
||||||
|
"options": "File",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_dlnw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "is_scorm_package",
|
||||||
|
"fieldname": "scorm_package_path",
|
||||||
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -59,7 +111,7 @@
|
|||||||
"link_fieldname": "chapter"
|
"link_fieldname": "chapter"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-09-29 17:03:58.013819",
|
"modified": "2024-11-15 12:03:31.370943",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Chapter",
|
"name": "Course Chapter",
|
||||||
@@ -79,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",
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils.telemetry import capture
|
from lms.lms.utils import get_course_progress
|
||||||
|
from lms.lms.api import update_course_statistics
|
||||||
|
|
||||||
|
|
||||||
class CourseChapter(Document):
|
class CourseChapter(Document):
|
||||||
pass
|
def on_update(self):
|
||||||
|
self.recalculate_course_progress()
|
||||||
|
update_course_statistics()
|
||||||
|
|
||||||
|
def recalculate_course_progress(self):
|
||||||
|
previous_lessons = (
|
||||||
|
self.get_doc_before_save() and self.get_doc_before_save().as_dict().lessons
|
||||||
|
)
|
||||||
|
current_lessons = self.lessons
|
||||||
|
|
||||||
|
if previous_lessons and previous_lessons != current_lessons:
|
||||||
|
enrolled_members = frappe.get_all(
|
||||||
|
"LMS Enrollment", {"course": self.course}, ["member", "name"]
|
||||||
|
)
|
||||||
|
for enrollment in enrolled_members:
|
||||||
|
new_progress = get_course_progress(self.course, enrollment.member)
|
||||||
|
frappe.db.set_value("LMS Enrollment", enrollment.name, "progress", new_progress)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -193,13 +193,15 @@
|
|||||||
"depends_on": "paid_batch",
|
"depends_on": "paid_batch",
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount"
|
"label": "Amount",
|
||||||
|
"mandatory_depends_on": "paid_batch"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_batch",
|
"depends_on": "paid_batch",
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
|
"mandatory_depends_on": "paid_batch",
|
||||||
"options": "Currency"
|
"options": "Currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -328,7 +330,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-18 18:06:37.229885",
|
"modified": "2024-11-18 16:28:41.336928",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ class LMSBatch(Document):
|
|||||||
self.validate_duplicate_courses()
|
self.validate_duplicate_courses()
|
||||||
self.validate_duplicate_students()
|
self.validate_duplicate_students()
|
||||||
self.validate_payments_app()
|
self.validate_payments_app()
|
||||||
|
self.validate_amount_and_currency()
|
||||||
self.validate_duplicate_assessments()
|
self.validate_duplicate_assessments()
|
||||||
self.validate_membership()
|
self.validate_membership()
|
||||||
self.validate_timetable()
|
self.validate_timetable()
|
||||||
self.send_confirmation_mail()
|
self.send_confirmation_mail()
|
||||||
self.validate_evaluation_end_date()
|
self.validate_evaluation_end_date()
|
||||||
|
self.add_students_to_live_class()
|
||||||
|
|
||||||
def validate_batch_end_date(self):
|
def validate_batch_end_date(self):
|
||||||
if self.end_date < self.start_date:
|
if self.end_date < self.start_date:
|
||||||
@@ -63,6 +65,10 @@ class LMSBatch(Document):
|
|||||||
if "payments" not in installed_apps:
|
if "payments" not in installed_apps:
|
||||||
frappe.throw(_("Please install the Payments app to create a paid batches."))
|
frappe.throw(_("Please install the Payments app to create a paid batches."))
|
||||||
|
|
||||||
|
def validate_amount_and_currency(self):
|
||||||
|
if self.paid_batch and (not self.amount or not self.currency):
|
||||||
|
frappe.throw(_("Amount and currency are required for paid batches."))
|
||||||
|
|
||||||
def validate_duplicate_assessments(self):
|
def validate_duplicate_assessments(self):
|
||||||
assessments = [row.assessment_name for row in self.assessment]
|
assessments = [row.assessment_name for row in self.assessment]
|
||||||
for assessment in self.assessment:
|
for assessment in self.assessment:
|
||||||
@@ -139,6 +145,27 @@ class LMSBatch(Document):
|
|||||||
if cint(self.seat_count) < len(self.students):
|
if cint(self.seat_count) < len(self.students):
|
||||||
frappe.throw(_("There are no seats available in this batch."))
|
frappe.throw(_("There are no seats available in this batch."))
|
||||||
|
|
||||||
|
def add_students_to_live_class(self):
|
||||||
|
for student in self.students:
|
||||||
|
if student.is_new():
|
||||||
|
live_classes = frappe.get_all(
|
||||||
|
"LMS Live Class", {"batch_name": self.name}, ["name", "event"]
|
||||||
|
)
|
||||||
|
|
||||||
|
for live_class in live_classes:
|
||||||
|
if live_class.event:
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Event Participants",
|
||||||
|
"reference_doctype": "User",
|
||||||
|
"reference_docname": student.student,
|
||||||
|
"email": student.student,
|
||||||
|
"parent": live_class.event,
|
||||||
|
"parenttype": "Event",
|
||||||
|
"parentfield": "event_participants",
|
||||||
|
}
|
||||||
|
).save()
|
||||||
|
|
||||||
def validate_timetable(self):
|
def validate_timetable(self):
|
||||||
for schedule in self.timetable:
|
for schedule in self.timetable:
|
||||||
if schedule.start_time and schedule.end_time:
|
if schedule.start_time and schedule.end_time:
|
||||||
|
|||||||
@@ -48,7 +48,12 @@
|
|||||||
"certification_section",
|
"certification_section",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
"column_break_rxww",
|
"column_break_rxww",
|
||||||
"expiry"
|
"expiry",
|
||||||
|
"tab_4_tab",
|
||||||
|
"statistics_section",
|
||||||
|
"enrollments",
|
||||||
|
"lessons",
|
||||||
|
"rating"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -249,6 +254,36 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Category",
|
"label": "Category",
|
||||||
"options": "LMS Category"
|
"options": "LMS Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tab_4_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Statistics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "statistics_section",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enrollments",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Enrollments",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "lessons",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Lessons",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "rating",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Rating",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -275,7 +310,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-09-21 10:23:58.633912",
|
"modified": "2024-10-30 23:08:31.842860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class LMSCourse(Document):
|
|||||||
self.validate_video_link()
|
self.validate_video_link()
|
||||||
self.validate_status()
|
self.validate_status()
|
||||||
self.validate_payments_app()
|
self.validate_payments_app()
|
||||||
|
self.validate_amount_and_currency()
|
||||||
self.image = validate_image(self.image)
|
self.image = validate_image(self.image)
|
||||||
|
|
||||||
def validate_published(self):
|
def validate_published(self):
|
||||||
@@ -51,6 +52,10 @@ class LMSCourse(Document):
|
|||||||
if "payments" not in installed_apps:
|
if "payments" not in installed_apps:
|
||||||
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
||||||
|
|
||||||
|
def validate_amount_and_currency(self):
|
||||||
|
if self.paid_course and (not self.amount and not self.currency):
|
||||||
|
frappe.throw(_("Amount and currency are required for paid courses."))
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||||
self.send_email_to_interested_users()
|
self.send_email_to_interested_users()
|
||||||
@@ -187,192 +192,3 @@ def reindex_exercises(doc):
|
|||||||
course = frappe.get_doc("LMS Course", course_data["name"])
|
course = frappe.get_doc("LMS Course", course_data["name"])
|
||||||
course.reindex_exercises()
|
course.reindex_exercises()
|
||||||
frappe.msgprint("All exercises in this course have been re-indexed.")
|
frappe.msgprint("All exercises in this course have been re-indexed.")
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def search_course(text):
|
|
||||||
courses = frappe.get_all(
|
|
||||||
"LMS Course",
|
|
||||||
filters={"published": True},
|
|
||||||
or_filters={
|
|
||||||
"title": ["like", f"%{text}%"],
|
|
||||||
"tags": ["like", f"%{text}%"],
|
|
||||||
"short_introduction": ["like", f"%{text}%"],
|
|
||||||
"description": ["like", f"%{text}%"],
|
|
||||||
},
|
|
||||||
fields=["name", "title"],
|
|
||||||
)
|
|
||||||
return courses
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def submit_for_review(course):
|
|
||||||
chapters = frappe.get_all("Chapter Reference", {"parent": course})
|
|
||||||
if not len(chapters):
|
|
||||||
return "No Chp"
|
|
||||||
frappe.db.set_value("LMS Course", course, "status", "Under Review")
|
|
||||||
return "OK"
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def save_course(
|
|
||||||
tags,
|
|
||||||
title,
|
|
||||||
short_introduction,
|
|
||||||
video_link,
|
|
||||||
description,
|
|
||||||
course,
|
|
||||||
published,
|
|
||||||
upcoming,
|
|
||||||
image=None,
|
|
||||||
paid_course=False,
|
|
||||||
course_price=None,
|
|
||||||
currency=None,
|
|
||||||
):
|
|
||||||
if not can_create_courses(course):
|
|
||||||
return
|
|
||||||
|
|
||||||
if course:
|
|
||||||
doc = frappe.get_doc("LMS Course", course)
|
|
||||||
else:
|
|
||||||
doc = frappe.get_doc({"doctype": "LMS Course"})
|
|
||||||
|
|
||||||
doc.update(
|
|
||||||
{
|
|
||||||
"title": title,
|
|
||||||
"short_introduction": short_introduction,
|
|
||||||
"video_link": video_link,
|
|
||||||
"image": image,
|
|
||||||
"description": description,
|
|
||||||
"tags": tags,
|
|
||||||
"published": cint(published),
|
|
||||||
"upcoming": cint(upcoming),
|
|
||||||
"paid_course": cint(paid_course),
|
|
||||||
"course_price": course_price,
|
|
||||||
"currency": currency,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
return doc.name
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def save_chapter(course, title, chapter_description, idx, chapter):
|
|
||||||
if chapter:
|
|
||||||
doc = frappe.get_doc("Course Chapter", chapter)
|
|
||||||
else:
|
|
||||||
doc = frappe.get_doc({"doctype": "Course Chapter"})
|
|
||||||
|
|
||||||
doc.update({"course": course, "title": title, "description": chapter_description})
|
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
if chapter:
|
|
||||||
chapter_reference = frappe.get_doc("Chapter Reference", {"chapter": chapter})
|
|
||||||
else:
|
|
||||||
chapter_reference = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Chapter Reference",
|
|
||||||
"parent": course,
|
|
||||||
"parenttype": "LMS Course",
|
|
||||||
"parentfield": "chapters",
|
|
||||||
"idx": idx,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
chapter_reference.update({"chapter": doc.name})
|
|
||||||
chapter_reference.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
return doc.name
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def save_lesson(
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
chapter,
|
|
||||||
preview,
|
|
||||||
idx,
|
|
||||||
lesson,
|
|
||||||
instructor_notes=None,
|
|
||||||
youtube=None,
|
|
||||||
quiz_id=None,
|
|
||||||
question=None,
|
|
||||||
file_type=None,
|
|
||||||
):
|
|
||||||
if lesson:
|
|
||||||
doc = frappe.get_doc("Course Lesson", lesson)
|
|
||||||
else:
|
|
||||||
doc = frappe.get_doc({"doctype": "Course Lesson"})
|
|
||||||
|
|
||||||
doc.update(
|
|
||||||
{
|
|
||||||
"chapter": chapter,
|
|
||||||
"title": title,
|
|
||||||
"body": body,
|
|
||||||
"instructor_notes": instructor_notes,
|
|
||||||
"include_in_preview": preview,
|
|
||||||
"youtube": youtube,
|
|
||||||
"quiz_id": quiz_id,
|
|
||||||
"question": question,
|
|
||||||
"file_type": file_type,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
if lesson:
|
|
||||||
lesson_reference = frappe.get_doc("Lesson Reference", {"lesson": lesson})
|
|
||||||
else:
|
|
||||||
lesson_reference = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Lesson Reference",
|
|
||||||
"parent": chapter,
|
|
||||||
"parenttype": "Course Chapter",
|
|
||||||
"parentfield": "lessons",
|
|
||||||
"idx": idx,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
lesson_reference.update({"lesson": doc.name})
|
|
||||||
lesson_reference.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
return doc.name
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def reorder_lesson(old_chapter, old_lesson_array, new_chapter, new_lesson_array):
|
|
||||||
if old_chapter == new_chapter:
|
|
||||||
sort_lessons(new_chapter, new_lesson_array)
|
|
||||||
else:
|
|
||||||
sort_lessons(old_chapter, old_lesson_array)
|
|
||||||
sort_lessons(new_chapter, new_lesson_array)
|
|
||||||
|
|
||||||
|
|
||||||
def sort_lessons(chapter, lesson_array):
|
|
||||||
lesson_array = json.loads(lesson_array)
|
|
||||||
for les in lesson_array:
|
|
||||||
ref = frappe.get_all("Lesson Reference", {"lesson": les}, ["name", "idx"])
|
|
||||||
if ref:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Lesson Reference",
|
|
||||||
ref[0].name,
|
|
||||||
{
|
|
||||||
"parent": chapter,
|
|
||||||
"idx": lesson_array.index(les) + 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def reorder_chapter(chapter_array):
|
|
||||||
chapter_array = json.loads(chapter_array)
|
|
||||||
|
|
||||||
for chap in chapter_array:
|
|
||||||
ref = frappe.get_all("Chapter Reference", {"chapter": chap}, ["name", "idx"])
|
|
||||||
if ref:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Chapter Reference",
|
|
||||||
ref[0].name,
|
|
||||||
{
|
|
||||||
"idx": chapter_array.index(chap) + 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -75,7 +75,8 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Course",
|
"label": "Course",
|
||||||
"options": "LMS Course",
|
"options": "LMS Course",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "current_lesson",
|
"fieldname": "current_lesson",
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-14 14:50:08.405033",
|
"modified": "2024-10-30 12:44:16.103598",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Enrollment",
|
"name": "LMS Enrollment",
|
||||||
|
|||||||
@@ -10,19 +10,20 @@
|
|||||||
"title",
|
"title",
|
||||||
"host",
|
"host",
|
||||||
"batch_name",
|
"batch_name",
|
||||||
|
"event",
|
||||||
"column_break_astv",
|
"column_break_astv",
|
||||||
"date",
|
|
||||||
"time",
|
|
||||||
"duration",
|
|
||||||
"section_break_glxh",
|
|
||||||
"description",
|
"description",
|
||||||
|
"section_break_glxh",
|
||||||
|
"date",
|
||||||
|
"duration",
|
||||||
"column_break_spvt",
|
"column_break_spvt",
|
||||||
|
"time",
|
||||||
"timezone",
|
"timezone",
|
||||||
"password",
|
|
||||||
"auto_recording",
|
|
||||||
"section_break_yrpq",
|
"section_break_yrpq",
|
||||||
|
"password",
|
||||||
"start_url",
|
"start_url",
|
||||||
"column_break_yokr",
|
"column_break_yokr",
|
||||||
|
"auto_recording",
|
||||||
"join_url"
|
"join_url"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -122,11 +123,19 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Auto Recording",
|
"label": "Auto Recording",
|
||||||
"options": "No Recording\nLocal\nCloud"
|
"options": "No Recording\nLocal\nCloud"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "event",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Event",
|
||||||
|
"options": "Event",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-09 11:22:33.272341",
|
"modified": "2024-11-11 18:59:26.396111",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Live Class",
|
"name": "LMS Live Class",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class LMSLiveClass(Document):
|
|||||||
if calendar:
|
if calendar:
|
||||||
event = self.create_event()
|
event = self.create_event()
|
||||||
self.add_event_participants(event, calendar)
|
self.add_event_participants(event, calendar)
|
||||||
|
frappe.db.set_value(self.doctype, self.name, "event", event.name)
|
||||||
|
|
||||||
def create_event(self):
|
def create_event(self):
|
||||||
start = f"{self.date} {self.time}"
|
start = f"{self.date} {self.time}"
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "payment_received",
|
"fieldname": "payment_received",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Payment Received"
|
"label": "Payment Received"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -140,7 +141,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-26 16:54:12.408274",
|
"modified": "2024-10-31 15:33:39.420366",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Payment",
|
"name": "LMS Payment",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class LMSQuestion(Document):
|
|||||||
def validate_correct_answers(question):
|
def validate_correct_answers(question):
|
||||||
if question.type == "Choices":
|
if question.type == "Choices":
|
||||||
validate_duplicate_options(question)
|
validate_duplicate_options(question)
|
||||||
|
validate_minimum_options(question)
|
||||||
validate_correct_options(question)
|
validate_correct_options(question)
|
||||||
elif question.type == "User Input":
|
elif question.type == "User Input":
|
||||||
validate_possible_answer(question)
|
validate_possible_answer(question)
|
||||||
@@ -42,6 +43,11 @@ def validate_correct_options(question):
|
|||||||
frappe.throw(_("At least one option must be correct for this question."))
|
frappe.throw(_("At least one option must be correct for this question."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_minimum_options(question):
|
||||||
|
if question.type == "Choices" and (not question.option_1 or not question.option_2):
|
||||||
|
frappe.throw(_("Minimum two options are required for multiple choice questions."))
|
||||||
|
|
||||||
|
|
||||||
def validate_possible_answer(question):
|
def validate_possible_answer(question):
|
||||||
possible_answers = []
|
possible_answers = []
|
||||||
possible_answers_fields = [
|
possible_answers_fields = [
|
||||||
|
|||||||
@@ -57,14 +57,15 @@ class LMSQuiz(Document):
|
|||||||
types = [question.type for question in self.questions]
|
types = [question.type for question in self.questions]
|
||||||
types = set(types)
|
types = set(types)
|
||||||
|
|
||||||
if "Open Ended" in types and len(types) > 1:
|
if "Open Ended" in types:
|
||||||
frappe.throw(
|
if len(types) > 1:
|
||||||
_(
|
frappe.throw(
|
||||||
"If you want open ended questions then make sure each question in the quiz is of open ended type."
|
_(
|
||||||
|
"If you want open ended questions then make sure each question in the quiz is of open ended type."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
else:
|
||||||
else:
|
self.show_answers = 0
|
||||||
self.show_answers = 0
|
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
if not self.name:
|
if not self.name:
|
||||||
|
|||||||
@@ -86,32 +86,32 @@ def get_charts(data):
|
|||||||
|
|
||||||
completed = 0
|
completed = 0
|
||||||
less_than_hundred = 0
|
less_than_hundred = 0
|
||||||
less_than_seventy = 0
|
less_than_seventy_one = 0
|
||||||
less_than_forty = 0
|
less_than_forty_one = 0
|
||||||
less_than_ten = 0
|
less_than_eleven = 0
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
if row.progress == 100:
|
if row.progress == 100:
|
||||||
completed += 1
|
completed += 1
|
||||||
elif row.progress < 100 and row.progress > 70:
|
elif row.progress < 100 and row.progress > 70:
|
||||||
less_than_hundred += 1
|
less_than_hundred += 1
|
||||||
elif row.progress < 70 and row.progress > 40:
|
elif row.progress < 71 and row.progress > 40:
|
||||||
less_than_seventy += 1
|
less_than_seventy_one += 1
|
||||||
elif row.progress < 40 and row.progress > 10:
|
elif row.progress < 41 and row.progress > 10:
|
||||||
less_than_forty += 1
|
less_than_forty_one += 1
|
||||||
elif row.progress < 10:
|
elif row.progress < 11:
|
||||||
less_than_ten += 1
|
less_than_eleven += 1
|
||||||
|
|
||||||
charts = {
|
charts = {
|
||||||
"data": {
|
"data": {
|
||||||
"labels": ["0-10", "10-40", "40-70", "70-99", "100"],
|
"labels": ["0-10", "11-40", "41-70", "71-99", "100"],
|
||||||
"datasets": [
|
"datasets": [
|
||||||
{
|
{
|
||||||
"name": "Progress (%)",
|
"name": "Progress (%)",
|
||||||
"values": [
|
"values": [
|
||||||
less_than_ten,
|
less_than_eleven,
|
||||||
less_than_forty,
|
less_than_forty_one,
|
||||||
less_than_seventy,
|
less_than_seventy_one,
|
||||||
less_than_hundred,
|
less_than_hundred,
|
||||||
completed,
|
completed,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def get_chapters(course):
|
|||||||
chapter_details = frappe.db.get_value(
|
chapter_details = frappe.db.get_value(
|
||||||
"Course Chapter",
|
"Course Chapter",
|
||||||
{"name": chapter.chapter},
|
{"name": chapter.chapter},
|
||||||
["name", "title", "description"],
|
["name", "title"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
chapter.update(chapter_details)
|
chapter.update(chapter_details)
|
||||||
@@ -157,11 +157,12 @@ def get_lesson_details(chapter, progress=False):
|
|||||||
"file_type",
|
"file_type",
|
||||||
"instructor_notes",
|
"instructor_notes",
|
||||||
"course",
|
"course",
|
||||||
|
"content",
|
||||||
],
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
lesson_details.number = f"{chapter.idx}.{row.idx}"
|
lesson_details.number = f"{chapter.idx}.{row.idx}"
|
||||||
lesson_details.icon = get_lesson_icon(lesson_details.body)
|
lesson_details.icon = get_lesson_icon(lesson_details.body, lesson_details.content)
|
||||||
|
|
||||||
if progress:
|
if progress:
|
||||||
lesson_details.is_complete = get_progress(lesson_details.course, lesson_details.name)
|
lesson_details.is_complete = get_progress(lesson_details.course, lesson_details.name)
|
||||||
@@ -170,20 +171,38 @@ def get_lesson_details(chapter, progress=False):
|
|||||||
return lessons
|
return lessons
|
||||||
|
|
||||||
|
|
||||||
def get_lesson_icon(content):
|
def get_lesson_icon(body, content):
|
||||||
icon = None
|
if content:
|
||||||
macros = find_macros(content)
|
content = json.loads(content)
|
||||||
|
|
||||||
|
for block in content.get("blocks"):
|
||||||
|
if block.get("type") == "upload" and block.get("data").get("file_type").lower() in [
|
||||||
|
"mp4",
|
||||||
|
"webm",
|
||||||
|
"ogg",
|
||||||
|
"mov",
|
||||||
|
]:
|
||||||
|
return "icon-youtube"
|
||||||
|
|
||||||
|
if block.get("type") == "embed" and block.get("data").get("service") in [
|
||||||
|
"youtube",
|
||||||
|
"vimeo",
|
||||||
|
]:
|
||||||
|
return "icon-youtube"
|
||||||
|
|
||||||
|
if block.get("type") == "quiz":
|
||||||
|
return "icon-quiz"
|
||||||
|
|
||||||
|
return "icon-list"
|
||||||
|
|
||||||
|
macros = find_macros(body)
|
||||||
for macro in macros:
|
for macro in macros:
|
||||||
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
|
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
|
||||||
icon = "icon-youtube"
|
return "icon-youtube"
|
||||||
elif macro[0] == "Quiz":
|
elif macro[0] == "Quiz":
|
||||||
icon = "icon-quiz"
|
return "icon-quiz"
|
||||||
|
|
||||||
if not icon:
|
return "icon-list"
|
||||||
icon = "icon-list"
|
|
||||||
|
|
||||||
return icon
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@@ -484,11 +503,6 @@ def first_lesson_exists(course):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def redirect_to_courses_list():
|
|
||||||
frappe.local.flags.redirect_location = "/lms/courses"
|
|
||||||
raise frappe.Redirect
|
|
||||||
|
|
||||||
|
|
||||||
def has_course_instructor_role(member=None):
|
def has_course_instructor_role(member=None):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Has Role",
|
"Has Role",
|
||||||
@@ -1027,23 +1041,13 @@ def get_course_details(course):
|
|||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
|
"lessons",
|
||||||
|
"enrollments",
|
||||||
|
"rating",
|
||||||
],
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
course_details.tags = course_details.tags.split(",") if course_details.tags else []
|
course_details.tags = course_details.tags.split(",") if course_details.tags else []
|
||||||
course_details.lesson_count = get_lesson_count(course_details.name)
|
|
||||||
|
|
||||||
course_details.enrollment_count = frappe.db.count(
|
|
||||||
"LMS Enrollment", {"course": course_details.name, "member_type": "Student"}
|
|
||||||
)
|
|
||||||
course_details.enrollment_count_formatted = format_number(
|
|
||||||
course_details.enrollment_count
|
|
||||||
)
|
|
||||||
|
|
||||||
avg_rating = get_average_rating(course_details.name) or 0
|
|
||||||
course_details.avg_rating = flt(
|
|
||||||
avg_rating, frappe.get_system_settings("float_precision") or 3
|
|
||||||
)
|
|
||||||
|
|
||||||
course_details.instructors = get_instructors(course_details.name)
|
course_details.instructors = get_instructors(course_details.name)
|
||||||
if course_details.paid_course:
|
if course_details.paid_course:
|
||||||
@@ -1092,14 +1096,14 @@ def get_categorized_courses(courses):
|
|||||||
):
|
):
|
||||||
new.append(course)
|
new.append(course)
|
||||||
|
|
||||||
if course.membership and course.published:
|
if course.membership:
|
||||||
enrolled.append(course)
|
enrolled.append(course)
|
||||||
elif course.is_instructor:
|
elif course.is_instructor:
|
||||||
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: cint(x.enrollments), reverse=True)
|
||||||
|
|
||||||
live.sort(key=lambda x: x.featured, reverse=True)
|
live.sort(key=lambda x: x.featured, reverse=True)
|
||||||
|
|
||||||
@@ -1124,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", "description"],
|
["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
|
||||||
|
|
||||||
@@ -1142,8 +1155,14 @@ 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 or lesson_details.is_scorm_package:
|
||||||
|
return {}
|
||||||
|
|
||||||
membership = get_membership(course)
|
membership = get_membership(course)
|
||||||
course_title = frappe.db.get_value("LMS Course", course, "title")
|
course_title = frappe.db.get_value("LMS Course", course, "title")
|
||||||
if (
|
if (
|
||||||
@@ -1258,7 +1277,7 @@ def get_batch_details(batch):
|
|||||||
batch_details.instructors = get_instructors(batch)
|
batch_details.instructors = get_instructors(batch)
|
||||||
|
|
||||||
batch_details.courses = frappe.get_all(
|
batch_details.courses = frappe.get_all(
|
||||||
"Batch Course", filters={"parent": batch}, fields=["course", "title"]
|
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
|
||||||
)
|
)
|
||||||
batch_details.students = frappe.get_all(
|
batch_details.students = frappe.get_all(
|
||||||
"Batch Student", {"parent": batch}, pluck="student"
|
"Batch Student", {"parent": batch}, pluck="student"
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
frappe.ready(function () {
|
|
||||||
frappe.web_form.after_save = () => {
|
|
||||||
let data = frappe.web_form.get_values();
|
|
||||||
let slug = new URLSearchParams(window.location.search).get("slug");
|
|
||||||
frappe.msgprint({
|
|
||||||
message: __("Batch {0} has been successfully created!", [
|
|
||||||
data.title,
|
|
||||||
]),
|
|
||||||
clear: true,
|
|
||||||
});
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.href = `courses/${slug}`;
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
frappe.web_form.validate = () => {
|
|
||||||
let sysdefaults = frappe.boot.sysdefaults;
|
|
||||||
let time_format =
|
|
||||||
sysdefaults && sysdefaults.time_format
|
|
||||||
? sysdefaults.time_format
|
|
||||||
: "HH:mm:ss";
|
|
||||||
let data = frappe.web_form.get_values();
|
|
||||||
|
|
||||||
data.start_time = moment(data.start_time, time_format).format(
|
|
||||||
time_format
|
|
||||||
);
|
|
||||||
data.end_time = moment(data.end_time, time_format).format(time_format);
|
|
||||||
|
|
||||||
if (data.start_date < frappe.datetime.nowdate()) {
|
|
||||||
frappe.msgprint(__("Start date cannot be a past date."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!frappe.datetime.validate(data.start_time) ||
|
|
||||||
!frappe.datetime.validate(data.end_time)
|
|
||||||
) {
|
|
||||||
frappe.msgprint(__("Invalid Start or End Time."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.start_time > data.end_time) {
|
|
||||||
frappe.msgprint(__("Start Time should be less than End Time."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
{
|
|
||||||
"accept_payment": 0,
|
|
||||||
"allow_comments": 0,
|
|
||||||
"allow_delete": 0,
|
|
||||||
"allow_edit": 0,
|
|
||||||
"allow_incomplete": 0,
|
|
||||||
"allow_multiple": 0,
|
|
||||||
"allow_print": 0,
|
|
||||||
"amount": 0.0,
|
|
||||||
"amount_based_on_field": 0,
|
|
||||||
"apply_document_permissions": 0,
|
|
||||||
"button_label": "Save",
|
|
||||||
"creation": "2021-04-20 11:37:49.135114",
|
|
||||||
"custom_css": ".datepicker.active {\n background-color: white;\n}\n\n[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
|
|
||||||
"doc_type": "LMS Batch Old",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Web Form",
|
|
||||||
"idx": 0,
|
|
||||||
"is_standard": 1,
|
|
||||||
"login_required": 1,
|
|
||||||
"max_attachment_size": 0,
|
|
||||||
"modified": "2021-06-15 18:49:50.530002",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "add-a-new-batch",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"payment_button_label": "Buy Now",
|
|
||||||
"published": 1,
|
|
||||||
"route": "add-a-new-batch",
|
|
||||||
"route_to_success_link": 0,
|
|
||||||
"show_attachments": 0,
|
|
||||||
"show_in_grid": 0,
|
|
||||||
"show_sidebar": 0,
|
|
||||||
"sidebar_items": [],
|
|
||||||
"success_url": "/add-a-new-batch",
|
|
||||||
"title": "Add a new batch",
|
|
||||||
"web_form_fields": [
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "course",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Course",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "LMS Course",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Title",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "start_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Start Date",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"description": "",
|
|
||||||
"fieldname": "sessions_on",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Sessions On Days",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "start_time",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Start Time",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "end_time",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "End Time",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
# do your magic here
|
|
||||||
pass
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
frappe.ready(function () {
|
|
||||||
frappe.web_form.after_save = () => {
|
|
||||||
let data = frappe.web_form.get_values();
|
|
||||||
if (data.class) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/batches/${data.class}`;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
{
|
|
||||||
"accept_payment": 0,
|
|
||||||
"allow_comments": 0,
|
|
||||||
"allow_delete": 0,
|
|
||||||
"allow_edit": 0,
|
|
||||||
"allow_incomplete": 0,
|
|
||||||
"allow_multiple": 1,
|
|
||||||
"allow_print": 0,
|
|
||||||
"amount": 0.0,
|
|
||||||
"amount_based_on_field": 0,
|
|
||||||
"anonymous": 0,
|
|
||||||
"apply_document_permissions": 0,
|
|
||||||
"button_label": "Save",
|
|
||||||
"creation": "2022-11-23 11:59:33.533053",
|
|
||||||
"doc_type": "LMS Certificate Evaluation",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Web Form",
|
|
||||||
"idx": 1,
|
|
||||||
"introduction_text": "",
|
|
||||||
"is_standard": 1,
|
|
||||||
"list_columns": [],
|
|
||||||
"login_required": 1,
|
|
||||||
"max_attachment_size": 0,
|
|
||||||
"modified": "2023-08-23 14:37:03.086305",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "evaluation",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"payment_button_label": "Buy Now",
|
|
||||||
"published": 1,
|
|
||||||
"route": "evaluation",
|
|
||||||
"show_attachments": 0,
|
|
||||||
"show_list": 1,
|
|
||||||
"show_sidebar": 0,
|
|
||||||
"title": "Evaluation",
|
|
||||||
"web_form_fields": [
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 1,
|
|
||||||
"fieldname": "member",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Member",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "User",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 1,
|
|
||||||
"fieldname": "course",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Course",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "LMS Course",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "batch_name",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Batch Name",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "LMS Batch",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Date",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "start_time",
|
|
||||||
"fieldtype": "Time",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Start Time",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "end_time",
|
|
||||||
"fieldtype": "Time",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "End Time",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Evaluation Details",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "rating",
|
|
||||||
"fieldtype": "Rating",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Rating",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "status",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Status",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Pass\nFail",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "summary",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Summary",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
pass
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
frappe.ready(function () {
|
|
||||||
frappe.web_form.after_load = () => {
|
|
||||||
redirect_to_user_profile_form();
|
|
||||||
add_listener_for_current_company();
|
|
||||||
add_listener_for_certificate_expiry();
|
|
||||||
add_listener_for_skill_add_rows();
|
|
||||||
add_listener_for_functions_add_rows();
|
|
||||||
add_listener_for_industries_add_rows();
|
|
||||||
};
|
|
||||||
|
|
||||||
frappe.web_form.validate = () => {
|
|
||||||
let information_missing;
|
|
||||||
const data = frappe.web_form.get_values();
|
|
||||||
if (data && data.work_experience && data.work_experience.length) {
|
|
||||||
data.work_experience.forEach((exp) => {
|
|
||||||
if (!exp.current && !exp.to_date) {
|
|
||||||
information_missing = true;
|
|
||||||
frappe.msgprint(
|
|
||||||
__("To Date is mandatory in Work Experience.")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (information_missing) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
frappe.web_form.after_save = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = `/profile_/${frappe.web_form.get_value([
|
|
||||||
"username",
|
|
||||||
])}`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirect_to_user_profile_form = () => {
|
|
||||||
if (!frappe.utils.get_url_arg("name")) {
|
|
||||||
window.location.href = `/edit-profile?name=${frappe.session.user}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const add_listener_for_current_company = () => {
|
|
||||||
$(document).on("click", "input[data-fieldname='current']", (e) => {
|
|
||||||
if ($(e.currentTarget).prop("checked"))
|
|
||||||
$("div[data-fieldname='to_date']").addClass("hide");
|
|
||||||
else $("div[data-fieldname='to_date']").removeClass("hide");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const add_listener_for_certificate_expiry = () => {
|
|
||||||
$(document).on("click", "input[data-fieldname='expire']", (e) => {
|
|
||||||
if ($(e.currentTarget).prop("checked"))
|
|
||||||
$("div[data-fieldname='expiration_date']").addClass("hide");
|
|
||||||
else $("div[data-fieldname='expiration_date']").removeClass("hide");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const add_listener_for_skill_add_rows = () => {
|
|
||||||
$('[data-fieldname="skill"]')
|
|
||||||
.find(".grid-add-row")
|
|
||||||
.click((e) => {
|
|
||||||
if ($('[data-fieldname="skill"]').find(".grid-row").length > 5) {
|
|
||||||
$('[data-fieldname="skill"]').find(".grid-add-row").hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const add_listener_for_functions_add_rows = () => {
|
|
||||||
$('[data-fieldname="preferred_functions"]')
|
|
||||||
.find(".grid-add-row")
|
|
||||||
.click((e) => {
|
|
||||||
if (
|
|
||||||
$('[data-fieldname="preferred_functions"]').find(".grid-row")
|
|
||||||
.length > 3
|
|
||||||
) {
|
|
||||||
$('[data-fieldname="preferred_functions"]')
|
|
||||||
.find(".grid-add-row")
|
|
||||||
.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const add_listener_for_industries_add_rows = () => {
|
|
||||||
$('[data-fieldname="preferred_industries"]')
|
|
||||||
.find(".grid-add-row")
|
|
||||||
.click((e) => {
|
|
||||||
if (
|
|
||||||
$('[data-fieldname="preferred_industries"]').find(".grid-row")
|
|
||||||
.length > 3
|
|
||||||
) {
|
|
||||||
$('[data-fieldname="preferred_industries"]')
|
|
||||||
.find(".grid-add-row")
|
|
||||||
.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
{
|
|
||||||
"accept_payment": 0,
|
|
||||||
"allow_comments": 0,
|
|
||||||
"allow_delete": 0,
|
|
||||||
"allow_edit": 1,
|
|
||||||
"allow_incomplete": 0,
|
|
||||||
"allow_multiple": 0,
|
|
||||||
"allow_print": 0,
|
|
||||||
"amount": 0.0,
|
|
||||||
"amount_based_on_field": 0,
|
|
||||||
"apply_document_permissions": 0,
|
|
||||||
"breadcrumbs": "",
|
|
||||||
"button_label": "Save",
|
|
||||||
"client_script": "",
|
|
||||||
"creation": "2021-06-30 13:48:13.682851",
|
|
||||||
"custom_css": "",
|
|
||||||
"doc_type": "User",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Web Form",
|
|
||||||
"idx": 0,
|
|
||||||
"is_standard": 1,
|
|
||||||
"list_columns": [],
|
|
||||||
"login_required": 1,
|
|
||||||
"max_attachment_size": 0,
|
|
||||||
"modified": "2023-01-09 15:45:11.411692",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "profile",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"payment_button_label": "Buy Now",
|
|
||||||
"published": 1,
|
|
||||||
"route": "edit-profile",
|
|
||||||
"show_attachments": 0,
|
|
||||||
"show_list": 0,
|
|
||||||
"show_sidebar": 0,
|
|
||||||
"success_url": "/profile",
|
|
||||||
"title": "Profile",
|
|
||||||
"web_form_fields": [
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "first_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "First Name",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "last_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Last Name",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "username",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Username",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"description": "Get your globally recognized avatar from Gravatar.com",
|
|
||||||
"fieldname": "user_image",
|
|
||||||
"fieldtype": "Attach Image",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "User Image",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"description": "",
|
|
||||||
"fieldname": "cover_image",
|
|
||||||
"fieldtype": "Attach Image",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Cover Image",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "city",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "City",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "mobile_no",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Mobile No",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Phone",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "headline",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Headline",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "linkedin",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "LinkedIn ID",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "github",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Github ID",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "medium",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Medium ID",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "looking_for_job",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "I am looking for a job",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "bio",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Bio",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "",
|
|
||||||
"fieldtype": "Page Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "education",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Education",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Education Detail",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "work_experience_details",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Work Experience",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "work_experience",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Work Experience",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Work Experience",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "internship",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Volunteering or Internship",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Work Experience",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "certification_details",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Certification Details",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "certification",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Certification",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Certification",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "skill_details",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Skill Details",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "skill",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Skill",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Skills",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
# do your magic here
|
|
||||||
pass
|
|
||||||
5543
lms/locale/ar.po
Normal file
5543
lms/locale/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/bs.po
Normal file
5543
lms/locale/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/de.po
Normal file
5543
lms/locale/de.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/eo.po
Normal file
5543
lms/locale/eo.po
Normal file
File diff suppressed because it is too large
Load Diff
8079
lms/locale/es.po
8079
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
5543
lms/locale/fa.po
Normal file
5543
lms/locale/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/fr.po
Normal file
5543
lms/locale/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/hu.po
Normal file
5543
lms/locale/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5543
lms/locale/pl.po
Normal file
5543
lms/locale/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/ru.po
Normal file
5543
lms/locale/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/sv.po
Normal file
5543
lms/locale/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/tr.po
Normal file
5543
lms/locale/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
5543
lms/locale/zh.po
Normal file
5543
lms/locale/zh.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -90,4 +90,7 @@ lms.patches.v1_0.set_published_on
|
|||||||
lms.patches.v2_0.fix_progress_percentage
|
lms.patches.v2_0.fix_progress_percentage
|
||||||
lms.patches.v2_0.add_discussion_topic_titles
|
lms.patches.v2_0.add_discussion_topic_titles
|
||||||
lms.patches.v2_0.sidebar_settings
|
lms.patches.v2_0.sidebar_settings
|
||||||
lms.patches.v2_0.delete_certificate_request_notification #18-09-2024
|
lms.patches.v2_0.delete_certificate_request_notification #18-09-2024
|
||||||
|
lms.patches.v2_0.add_course_statistics #21-10-2024
|
||||||
|
lms.patches.v2_0.give_discussions_permissions
|
||||||
|
lms.patches.v2_0.delete_web_forms
|
||||||
6
lms/patches/v2_0/add_course_statistics.py
Normal file
6
lms/patches/v2_0/add_course_statistics.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
from lms.lms.api import update_course_statistics
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
update_course_statistics()
|
||||||
5
lms/patches/v2_0/delete_web_forms.py
Normal file
5
lms/patches/v2_0/delete_web_forms.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.delete("Web Form", {"module": "LMS"})
|
||||||
6
lms/patches/v2_0/give_discussions_permissions.py
Normal file
6
lms/patches/v2_0/give_discussions_permissions.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
from lms.lms.api import give_dicussions_permission
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
give_dicussions_permission()
|
||||||
Reference in New Issue
Block a user