Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb3d8e4f7d | ||
|
|
0c6029cbe8 | ||
|
|
a643e9ae83 | ||
|
|
08ac3948c3 | ||
|
|
78d289b9c0 | ||
|
|
3473bdb527 | ||
|
|
a7f8835222 | ||
|
|
d6441955fc | ||
|
|
67d265e864 | ||
|
|
17031f1df0 | ||
|
|
234a24baa2 | ||
|
|
9a58f4688b | ||
|
|
87c1c928ba | ||
|
|
493b8297ea | ||
|
|
4d16602190 | ||
|
|
89222b23c3 | ||
|
|
89a181c7d5 | ||
|
|
c0aecf30c1 | ||
|
|
fc8ef21802 | ||
|
|
c45da4313e | ||
|
|
3a1a843747 | ||
|
|
c51e7b0037 | ||
|
|
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 | ||
|
|
af0cc7126b | ||
|
|
a085050d27 | ||
|
|
2442f35f56 | ||
|
|
ed79ea536b | ||
|
|
b3d0aecd14 | ||
|
|
5f43e67c0b | ||
|
|
49a765a9a6 | ||
|
|
4d82bc86e8 | ||
|
|
8fe02b83b8 | ||
|
|
9c9075606b | ||
|
|
53285a0d19 | ||
|
|
9cdeaebb47 | ||
|
|
a9cb52c68b | ||
|
|
f33e950e83 | ||
|
|
9c9b5963fe | ||
|
|
1597054cc9 | ||
|
|
deba6aa845 | ||
|
|
2d8ba3b84e | ||
|
|
e56b28abad | ||
|
|
eb350c5a20 | ||
|
|
961d5ec77b | ||
|
|
fa566514aa | ||
|
|
6e97449bf7 | ||
|
|
016dafb3c3 | ||
|
|
675bcc8956 | ||
|
|
aba4c034fc | ||
|
|
c76d8c582f | ||
|
|
f1cb0e6f3c | ||
|
|
d296687456 | ||
|
|
5b68001c94 | ||
|
|
8b1d9bb5a9 |
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://test_site_ui:8000",
|
baseUrl: "http://test: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
|
||||||
|
|||||||
@@ -18,11 +18,12 @@
|
|||||||
"@editorjs/nested-list": "^1.4.2",
|
"@editorjs/nested-list": "^1.4.2",
|
||||||
"@editorjs/paragraph": "^2.11.3",
|
"@editorjs/paragraph": "^2.11.3",
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
"@editorjs/simple-image": "^1.6.0",
|
||||||
|
"ace-builds": "^1.36.2",
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"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>
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ const props = defineProps({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
audio.value = document.querySelector('audio')
|
audio.value = document.querySelector('audio')
|
||||||
console.log(audio.value)
|
|
||||||
audio.value.onloadedmetadata = () => {
|
audio.value.onloadedmetadata = () => {
|
||||||
duration.value = audio.value.duration
|
duration.value = audio.value.duration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
v-else-if="batch.data.allow_self_enrollment && batch.data.seats_left"
|
v-else-if="batch.data.allow_self_enrollment && batch.data.seats_left"
|
||||||
|
@click="enrollInBatch()"
|
||||||
>
|
>
|
||||||
{{ __('Enroll Now') }}
|
{{ __('Enroll Now') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -97,11 +98,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject, computed } from 'vue'
|
import { inject, computed } from 'vue'
|
||||||
import { Badge, Button } from 'frappe-ui'
|
import { Badge, Button, createResource } from 'frappe-ui'
|
||||||
import { BookOpen, Clock, Globe } from 'lucide-vue-next'
|
import { BookOpen, Clock, Globe } from 'lucide-vue-next'
|
||||||
import { formatNumberIntoCurrency, formatTime } from '@/utils'
|
import { formatNumberIntoCurrency, formatTime, showToast } from '@/utils'
|
||||||
import DateRange from '@/components/Common/DateRange.vue'
|
import DateRange from '@/components/Common/DateRange.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -111,6 +114,39 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const enroll = createResource({
|
||||||
|
url: 'lms.lms.utils.enroll_in_batch',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
batch: props.batch.data.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const enrollInBatch = () => {
|
||||||
|
if (!user.data) {
|
||||||
|
window.location.href = `/login?redirect-to=/batches/details/${props.batch.data.name}`
|
||||||
|
}
|
||||||
|
enroll.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
showToast(
|
||||||
|
__('Success'),
|
||||||
|
__('You have been enrolled in this batch'),
|
||||||
|
'check'
|
||||||
|
)
|
||||||
|
router.push({
|
||||||
|
name: 'Batch',
|
||||||
|
params: {
|
||||||
|
batchName: props.batch.data.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const seats_left = computed(() => {
|
const seats_left = computed(() => {
|
||||||
if (props.batch.data?.seat_count) {
|
if (props.batch.data?.seat_count) {
|
||||||
return props.batch.data?.seat_count - props.batch.data?.students?.length
|
return props.batch.data?.seat_count - props.batch.data?.students?.length
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between min-h-0">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="font-semibold mb-1">
|
<div class="font-semibold mb-1">
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="overflow-y-auto">
|
||||||
<SettingFields :fields="fields" :data="data.data" />
|
<SettingFields :fields="fields" :data="data.data" />
|
||||||
<div class="flex flex-row-reverse mt-auto">
|
<div class="flex flex-row-reverse mt-auto">
|
||||||
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Button, Badge } from 'frappe-ui'
|
import { createResource, Button, Badge } from 'frappe-ui'
|
||||||
@@ -70,9 +72,16 @@ const update = () => {
|
|||||||
fieldsToSave[f.name] = f.value
|
fieldsToSave[f.name] = f.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
saveSettings.submit({
|
saveSettings.submit(
|
||||||
|
{
|
||||||
fields: fieldsToSave,
|
fields: fieldsToSave,
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
isDirty.value = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(props.data, (newData) => {
|
watch(props.data, (newData) => {
|
||||||
|
|||||||
204
frontend/src/components/Controls/CodeEditor.vue
Normal file
204
frontend/src/components/Controls/CodeEditor.vue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="editor flex flex-col gap-1"
|
||||||
|
:style="{
|
||||||
|
height: height,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="text-xs" v-if="label">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
ref="editor"
|
||||||
|
class="h-auto flex-1 overflow-hidden overscroll-none !rounded border border-outline-gray-2 bg-surface-gray-2 dark:bg-gray-900"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="mt-1 text-xs text-gray-600"
|
||||||
|
v-show="description"
|
||||||
|
v-html="description"
|
||||||
|
></span>
|
||||||
|
<Button
|
||||||
|
v-if="showSaveButton"
|
||||||
|
@click="emit('save', aceEditor?.getValue())"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDark } from '@vueuse/core'
|
||||||
|
import ace from 'ace-builds'
|
||||||
|
import 'ace-builds/src-min-noconflict/ext-searchbox'
|
||||||
|
import 'ace-builds/src-min-noconflict/theme-chrome'
|
||||||
|
import 'ace-builds/src-min-noconflict/theme-twilight'
|
||||||
|
import { PropType, onMounted, ref, watch } from 'vue'
|
||||||
|
import { Button } from 'frappe-ui'
|
||||||
|
|
||||||
|
const isDark = useDark({
|
||||||
|
attribute: 'data-theme',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [Object, String, Array],
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String as PropType<'JSON' | 'HTML' | 'Python' | 'JavaScript' | 'CSS'>,
|
||||||
|
default: 'JSON',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '250px',
|
||||||
|
},
|
||||||
|
showLineNumbers: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
autofocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showSaveButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['save', 'update:modelValue'])
|
||||||
|
const editor = ref<HTMLElement | null>(null)
|
||||||
|
let aceEditor = null as ace.Ace.Editor | null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupEditor()
|
||||||
|
})
|
||||||
|
|
||||||
|
const setupEditor = () => {
|
||||||
|
aceEditor = ace.edit(editor.value as HTMLElement)
|
||||||
|
resetEditor(props.modelValue as string, true)
|
||||||
|
aceEditor.setReadOnly(props.readonly)
|
||||||
|
aceEditor.setOptions({
|
||||||
|
fontSize: '12px',
|
||||||
|
useWorker: false,
|
||||||
|
showGutter: props.showLineNumbers,
|
||||||
|
wrap: props.showLineNumbers,
|
||||||
|
})
|
||||||
|
if (props.type === 'CSS') {
|
||||||
|
import('ace-builds/src-noconflict/mode-css').then(() => {
|
||||||
|
aceEditor?.session.setMode('ace/mode/css')
|
||||||
|
})
|
||||||
|
} else if (props.type === 'JavaScript') {
|
||||||
|
import('ace-builds/src-noconflict/mode-javascript').then(() => {
|
||||||
|
aceEditor?.session.setMode('ace/mode/javascript')
|
||||||
|
})
|
||||||
|
} else if (props.type === 'Python') {
|
||||||
|
import('ace-builds/src-noconflict/mode-python').then(() => {
|
||||||
|
aceEditor?.session.setMode('ace/mode/python')
|
||||||
|
})
|
||||||
|
} else if (props.type === 'JSON') {
|
||||||
|
import('ace-builds/src-noconflict/mode-json').then(() => {
|
||||||
|
aceEditor?.session.setMode('ace/mode/json')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
import('ace-builds/src-noconflict/mode-html').then(() => {
|
||||||
|
aceEditor?.session.setMode('ace/mode/html')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
aceEditor.on('blur', () => {
|
||||||
|
try {
|
||||||
|
let value = aceEditor?.getValue() || ''
|
||||||
|
if (props.type === 'JSON') {
|
||||||
|
value = JSON.parse(value)
|
||||||
|
}
|
||||||
|
if (value === props.modelValue) return
|
||||||
|
if (!props.showSaveButton && !props.readonly) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModelValue = () => {
|
||||||
|
let value = props.modelValue || ''
|
||||||
|
try {
|
||||||
|
if (props.type === 'JSON' || typeof value === 'object') {
|
||||||
|
value = JSON.stringify(value, null, 2)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return value as string
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEditor(value: string, resetHistory = false) {
|
||||||
|
value = getModelValue()
|
||||||
|
aceEditor?.setValue(value)
|
||||||
|
aceEditor?.clearSelection()
|
||||||
|
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
|
||||||
|
props.autofocus && aceEditor?.focus()
|
||||||
|
if (resetHistory) {
|
||||||
|
aceEditor?.session.getUndoManager().reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(isDark, () => {
|
||||||
|
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.type,
|
||||||
|
() => {
|
||||||
|
setupEditor()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
|
resetEditor(props.modelValue as string)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
defineExpose({ resetEditor })
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.editor .ace_editor {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
.editor :deep(.ace_scrollbar-h) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.editor :deep(.ace_search) {
|
||||||
|
@apply dark:bg-gray-800 dark:text-gray-200;
|
||||||
|
@apply dark:border-gray-800;
|
||||||
|
}
|
||||||
|
.editor :deep(.ace_searchbtn) {
|
||||||
|
@apply dark:bg-gray-800 dark:text-gray-200;
|
||||||
|
@apply dark:border-gray-800;
|
||||||
|
}
|
||||||
|
.editor :deep(.ace_button) {
|
||||||
|
@apply dark:bg-gray-800 dark:text-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor :deep(.ace_search_field) {
|
||||||
|
@apply dark:bg-gray-900 dark:text-gray-200;
|
||||||
|
@apply dark:border-gray-800;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Star } from 'lucide-vue-next'
|
import { Star } from 'lucide-vue-next'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -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 { createToast } 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'
|
||||||
|
|
||||||
@@ -139,11 +137,11 @@ const video_link = computed(() => {
|
|||||||
|
|
||||||
function enrollStudent() {
|
function enrollStudent() {
|
||||||
if (!user.data) {
|
if (!user.data) {
|
||||||
createToast({
|
showToast(
|
||||||
title: 'Please Login',
|
__('Please Login'),
|
||||||
icon: 'alert-circle',
|
__('You need to login first to enroll for this course'),
|
||||||
iconClasses: 'text-yellow-600 bg-yellow-100',
|
'circle-warn'
|
||||||
})
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@@ -159,11 +157,11 @@ function enrollStudent() {
|
|||||||
capture('enrolled_in_course', {
|
capture('enrolled_in_course', {
|
||||||
course: props.course.data.name,
|
course: props.course.data.name,
|
||||||
})
|
})
|
||||||
createToast({
|
showToast(
|
||||||
title: 'Enrolled Successfully',
|
__('Success'),
|
||||||
icon: 'check',
|
__('You have been enrolled in this course'),
|
||||||
iconClasses: 'text-green-600 bg-green-100',
|
'check'
|
||||||
})
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'Lesson',
|
name: 'Lesson',
|
||||||
@@ -173,7 +171,7 @@ function enrollStudent() {
|
|||||||
lessonNumber: 1,
|
lessonNumber: 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, 3000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +204,6 @@ const certificate = createResource({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
console.log(data)
|
|
||||||
window.open(
|
window.open(
|
||||||
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||||
data.name
|
data.name
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
<Trash2
|
<Trash2
|
||||||
v-if="allowEdit"
|
v-if="allowEdit"
|
||||||
@click.prevent="trashLesson(lesson.name, chapter.name)"
|
@click.prevent="trashLesson(lesson.name, chapter.name)"
|
||||||
class="h-4 w-4 stroke-1.5 text-gray-700 ml-auto invisible group-hover:visible"
|
class="h-4 w-4 text-red-500 ml-auto invisible group-hover:visible"
|
||||||
/>
|
/>
|
||||||
<Check
|
<Check
|
||||||
v-if="lesson.is_complete"
|
v-if="lesson.is_complete"
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource } from 'frappe-ui'
|
||||||
import { ref } from 'vue'
|
import { ref, getCurrentInstance } 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 {
|
||||||
@@ -138,6 +138,8 @@ const route = useRoute()
|
|||||||
const expandAll = ref(true)
|
const expandAll = ref(true)
|
||||||
const showChapterModal = ref(false)
|
const showChapterModal = ref(false)
|
||||||
const currentChapter = ref(null)
|
const currentChapter = ref(null)
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
@@ -202,10 +204,24 @@ const updateLessonIndex = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const trashLesson = (lessonName, chapterName) => {
|
const trashLesson = (lessonName, chapterName) => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Delete Lesson'),
|
||||||
|
message: __('Are you sure you want to delete this lesson?'),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Delete'),
|
||||||
|
theme: 'red',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(close) {
|
||||||
deleteLesson.submit({
|
deleteLesson.submit({
|
||||||
lesson: lessonName,
|
lesson: lessonName,
|
||||||
chapter: chapterName,
|
chapter: chapterName,
|
||||||
})
|
})
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openChapterDetail = (index) => {
|
const openChapterDetail = (index) => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
label="Title"
|
label="Title"
|
||||||
v-model="chapter.title"
|
v-model="chapter.title"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -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,11 +154,13 @@ function submitEvaluation(close) {
|
|||||||
const getCourses = () => {
|
const getCourses = () => {
|
||||||
let courses = []
|
let courses = []
|
||||||
for (const course of props.courses) {
|
for (const course of props.courses) {
|
||||||
|
if (course.evaluator) {
|
||||||
courses.push({
|
courses.push({
|
||||||
label: course.title,
|
label: course.title,
|
||||||
value: course.course,
|
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
|
||||||
|
|||||||
@@ -179,26 +179,6 @@ const tabsStructure = computed(() => {
|
|||||||
name: 'app_name',
|
name: 'app_name',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Copyright',
|
|
||||||
name: 'copyright',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Address',
|
|
||||||
name: 'address',
|
|
||||||
type: 'textarea',
|
|
||||||
rows: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Footer "Powered By"',
|
|
||||||
name: 'footer_powered',
|
|
||||||
type: 'textarea',
|
|
||||||
rows: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Column Break',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Logo',
|
label: 'Logo',
|
||||||
name: 'banner_image',
|
name: 'banner_image',
|
||||||
@@ -214,6 +194,23 @@ const tabsStructure = computed(() => {
|
|||||||
name: 'footer_logo',
|
name: 'footer_logo',
|
||||||
type: 'Upload',
|
type: 'Upload',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Address',
|
||||||
|
name: 'address',
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Footer "Powered By"',
|
||||||
|
name: 'footer_powered',
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Copyright',
|
||||||
|
name: 'copyright',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -292,9 +289,11 @@ const tabsStructure = computed(() => {
|
|||||||
rows: 10,
|
rows: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ask user category',
|
label: 'Ask for Occupation',
|
||||||
name: 'user_category',
|
name: 'user_category',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
|
description:
|
||||||
|
'Enable this option to ask users to select their occupation during the signup process.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="quiz.data">
|
<div v-if="quiz.data">
|
||||||
<div class="bg-blue-100 py-2 px-2 mb-4 rounded-md text-sm text-blue-800">
|
<div
|
||||||
<div class="leading-relaxed">
|
class="bg-blue-100 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-blue-800"
|
||||||
|
>
|
||||||
|
<div class="leading-5">
|
||||||
{{
|
{{
|
||||||
__('This quiz consists of {0} questions.').format(questions.length)
|
__('This quiz consists of {0} questions.').format(questions.length)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="quiz.data?.duration" class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Please ensure that you complete all the questions in {0} minutes.'
|
||||||
|
).format(quiz.data.duration)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div v-if="quiz.data?.duration" class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'If you fail to do so, the quiz will be automatically submitted when the timer ends.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
<div v-if="quiz.data.passing_percentage" class="leading-relaxed">
|
<div v-if="quiz.data.passing_percentage" class="leading-relaxed">
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
@@ -22,14 +38,16 @@
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="quiz.data.time" class="leading-relaxed">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'The quiz has a time limit. For each question you will be given {0} seconds.'
|
|
||||||
).format(quiz.data.time)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="quiz.data.duration" class="flex items-center space-x-2 my-4">
|
||||||
|
<span class="text-gray-600 text-xs"> {{ __('Time') }}: </span>
|
||||||
|
<ProgressBar :progress="timerProgress" />
|
||||||
|
<span class="font-semibold">
|
||||||
|
{{ formatTimer(timer) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeQuestion == 0">
|
<div v-if="activeQuestion == 0">
|
||||||
<div class="border text-center p-20 rounded-md">
|
<div class="border text-center p-20 rounded-md">
|
||||||
<div class="font-semibold text-lg">
|
<div class="font-semibold text-lg">
|
||||||
@@ -63,7 +81,7 @@
|
|||||||
class="border rounded-md p-5"
|
class="border rounded-md p-5"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="text-sm">
|
<div class="text-sm text-gray-600">
|
||||||
<span class="mr-2">
|
<span class="mr-2">
|
||||||
{{ __('Question {0}').format(activeQuestion) }}:
|
{{ __('Question {0}').format(activeQuestion) }}:
|
||||||
</span>
|
</span>
|
||||||
@@ -162,8 +180,8 @@
|
|||||||
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>
|
||||||
<div class="flex items-center justify-between mt-5">
|
<div class="flex items-center justify-between mt-4">
|
||||||
<div>
|
<div class="text-sm text-gray-600">
|
||||||
{{
|
{{
|
||||||
__('Question {0} of {1}').format(
|
__('Question {0} of {1}').format(
|
||||||
activeQuestion,
|
activeQuestion,
|
||||||
@@ -250,20 +268,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Badge, Button, createResource, ListView, TextEditor } from 'frappe-ui'
|
import {
|
||||||
import { ref, watch, reactive, inject } from 'vue'
|
Badge,
|
||||||
|
Button,
|
||||||
|
createResource,
|
||||||
|
ListView,
|
||||||
|
TextEditor,
|
||||||
|
FormControl,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { ref, watch, reactive, inject, computed } from 'vue'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast } from '@/utils/'
|
||||||
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
||||||
import { timeAgo } from '@/utils'
|
import { timeAgo } from '@/utils'
|
||||||
import FormControl from 'frappe-ui/src/components/FormControl.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
const user = inject('$user')
|
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
const activeQuestion = ref(0)
|
const activeQuestion = ref(0)
|
||||||
const currentQuestion = ref('')
|
const currentQuestion = ref('')
|
||||||
const selectedOptions = reactive([0, 0, 0, 0])
|
const selectedOptions = reactive([0, 0, 0, 0])
|
||||||
const showAnswers = reactive([])
|
const showAnswers = reactive([])
|
||||||
let questions = reactive([])
|
let questions = reactive([])
|
||||||
const possibleAnswer = ref(null)
|
const possibleAnswer = ref(null)
|
||||||
|
const timer = ref(0)
|
||||||
|
let timerInterval = null
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
quizName: {
|
quizName: {
|
||||||
@@ -284,6 +311,7 @@ const quiz = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
populateQuestions()
|
populateQuestions()
|
||||||
|
setupTimer()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -299,6 +327,37 @@ const populateQuestions = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setupTimer = () => {
|
||||||
|
if (quiz.data.duration) {
|
||||||
|
timer.value = quiz.data.duration * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
timer.value--
|
||||||
|
if (timer.value == 0) {
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
submitQuiz()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTimer = (seconds) => {
|
||||||
|
const hrs = Math.floor(seconds / 3600)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')
|
||||||
|
const secs = (seconds % 60).toString().padStart(2, '0')
|
||||||
|
return hrs != '00' ? `${hrs}:${mins}:${secs}` : `${mins}:${secs}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const timerProgress = computed(() => {
|
||||||
|
return (timer.value / (quiz.data.duration * 60)) * 100
|
||||||
|
})
|
||||||
|
|
||||||
const shuffleArray = (array) => {
|
const shuffleArray = (array) => {
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1))
|
const j = Math.floor(Math.random() * (i + 1))
|
||||||
@@ -383,6 +442,7 @@ watch(
|
|||||||
const startQuiz = () => {
|
const startQuiz = () => {
|
||||||
activeQuestion.value = 1
|
activeQuestion.value = 1
|
||||||
localStorage.removeItem(quiz.data.title)
|
localStorage.removeItem(quiz.data.title)
|
||||||
|
if (quiz.data.duration) startTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
const markAnswer = (index) => {
|
const markAnswer = (index) => {
|
||||||
@@ -493,9 +553,15 @@ const submitQuiz = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createSubmission = () => {
|
const createSubmission = () => {
|
||||||
quizSubmission.reload().then(() => {
|
quizSubmission.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
||||||
})
|
if (quiz.data.duration) clearInterval(timerInterval)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetQuiz = () => {
|
const resetQuiz = () => {
|
||||||
@@ -504,6 +570,7 @@ const resetQuiz = () => {
|
|||||||
showAnswers.length = 0
|
showAnswers.length = 0
|
||||||
quizSubmission.reset()
|
quizSubmission.reset()
|
||||||
populateQuestions()
|
populateQuestions()
|
||||||
|
setupTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInstructions = (question) => {
|
const getInstructions = (question) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between h-full">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex itemsc-center justify-between">
|
<div class="flex itemsc-center justify-between">
|
||||||
<div class="font-semibold mb-1">
|
<div class="text-xl font-semibold leading-none mb-1">
|
||||||
{{ __(label) }}
|
{{ __(label) }}
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -17,17 +17,16 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-else-if="field.type == 'Code'">
|
<div v-else-if="field.type == 'Code'">
|
||||||
<div>
|
<CodeEditor
|
||||||
{{ __(field.label) }}
|
:label="__(field.label)"
|
||||||
</div>
|
type="HTML"
|
||||||
<Codemirror
|
description="The HTML you add here will be shown on your sign up page."
|
||||||
v-model:value="data[field.name]"
|
v-model="data[field.name]"
|
||||||
:height="200"
|
height="250px"
|
||||||
:options="{
|
class="shrink-0"
|
||||||
mode: field.mode,
|
:showLineNumbers="true"
|
||||||
theme: 'seti',
|
>
|
||||||
}"
|
</CodeEditor>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="field.type == 'Upload'">
|
<div v-else-if="field.type == 'Upload'">
|
||||||
@@ -53,9 +52,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex items-center text-sm">
|
<div class="flex items-center text-sm space-x-2">
|
||||||
<div class="border rounded-md p-2 mr-2">
|
<div
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
class="flex items-center justify-center rounded border border-outline-gray-1 w-[15rem] py-5"
|
||||||
|
>
|
||||||
|
<img :src="data[field.name]?.file_url" class="h-6 rounded" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-wrap">
|
<div class="flex flex-col flex-wrap">
|
||||||
<span class="break-all">
|
<span class="break-all">
|
||||||
@@ -73,6 +74,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
v-else-if="field.type == 'checkbox'"
|
||||||
|
size="sm"
|
||||||
|
:label="__(field.label)"
|
||||||
|
:description="__(field.description)"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
/>
|
||||||
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
:key="field.name"
|
:key="field.name"
|
||||||
@@ -88,14 +97,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FormControl, FileUploader, Button } from 'frappe-ui'
|
import { FormControl, FileUploader, Button, Switch } from 'frappe-ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { getFileSize, validateFile } from '@/utils'
|
import { getFileSize, validateFile } from '@/utils'
|
||||||
import { X, FileText } from 'lucide-vue-next'
|
import { X, FileText } from 'lucide-vue-next'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Codemirror from 'codemirror-editor-vue3'
|
import CodeEditor from '@/components/Controls/CodeEditor.vue'
|
||||||
import 'codemirror/theme/seti.css'
|
|
||||||
import 'codemirror/mode/htmlmixed/htmlmixed.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fields: {
|
fields: {
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
<video
|
<video
|
||||||
@timeupdate="updateTime"
|
@timeupdate="updateTime"
|
||||||
@ended="videoEnded"
|
@ended="videoEnded"
|
||||||
class="rounded-lg border border-gray-100"
|
@click="togglePlay"
|
||||||
|
oncontextmenu="return false"
|
||||||
|
class="rounded-lg border border-gray-100 group cursor-pointer"
|
||||||
ref="videoRef"
|
ref="videoRef"
|
||||||
>
|
>
|
||||||
<source :src="fileURL" :type="type" />
|
<source :src="fileURL" :type="type" />
|
||||||
</video>
|
</video>
|
||||||
<div
|
<div
|
||||||
class="flex items-center space-x-2 bg-gray-200 rounded-md p-0.5 absolute bottom-3 w-[98%] left-0 right-0 mx-auto"
|
class="flex items-center space-x-2 bg-gray-200 rounded-md p-0.5 absolute bottom-3 w-[98%] left-0 right-0 mx-auto invisible group-hover:visible"
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -106,6 +108,14 @@ const pauseVideo = () => {
|
|||||||
playing.value = false
|
playing.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const togglePlay = () => {
|
||||||
|
if (playing.value) {
|
||||||
|
pauseVideo()
|
||||||
|
} else {
|
||||||
|
playVideo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const videoEnded = () => {
|
const videoEnded = () => {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import router from './router'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import dayjs from '@/utils/dayjs'
|
import dayjs from '@/utils/dayjs'
|
||||||
|
import { createDialog } from '@/utils/dialogs'
|
||||||
import translationPlugin from './translation'
|
import translationPlugin from './translation'
|
||||||
import { usersStore } from './stores/user'
|
import { usersStore } from './stores/user'
|
||||||
import { sessionStore } from './stores/session'
|
import { sessionStore } from './stores/session'
|
||||||
@@ -36,3 +37,4 @@ let { isLoggedIn } = sessionStore()
|
|||||||
app.provide('$user', userResource)
|
app.provide('$user', userResource)
|
||||||
app.provide('$allUsers', allUsers)
|
app.provide('$allUsers', allUsers)
|
||||||
app.config.globalProperties.$user = userResource
|
app.config.globalProperties.$user = userResource
|
||||||
|
app.config.globalProperties.$dialog = createDialog
|
||||||
|
|||||||
@@ -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,42 +36,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div>
|
<div class="text-xs text-gray-600 mb-2">
|
||||||
|
{{ __('Meta Image') }}
|
||||||
|
</div>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
v-if="!batch.image"
|
v-if="!batch.image"
|
||||||
class="mt-4"
|
|
||||||
:fileTypes="['image/*']"
|
:fileTypes="['image/*']"
|
||||||
:validateFile="validateFile"
|
:validateFile="validateFile"
|
||||||
@success="(file) => saveImage(file)"
|
@success="(file) => saveImage(file)"
|
||||||
>
|
>
|
||||||
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
||||||
<div class="mb-4">
|
<div class="flex items-center">
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
<div class="border rounded-md w-fit py-5 px-20">
|
||||||
{{ uploading ? `Uploading ${progress}%` : 'Upload an image' }}
|
<Image class="size-5 stroke-1 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<Button @click="openFileSelector">
|
||||||
|
{{ __('Upload') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<div class="mt-2 text-gray-600 text-sm">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Appears when the batch URL is shared on any online platform'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else class="mb-4">
|
<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">
|
<img :src="batch.image.file_url" class="border rounded-md w-40" />
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
<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>
|
||||||
<div class="flex flex-col">
|
|
||||||
<span>
|
|
||||||
{{ batch.image.file_name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500 mt-1">
|
|
||||||
{{ getFileSize(batch.image.file_size) }}
|
|
||||||
</span>
|
|
||||||
</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>
|
||||||
@@ -75,18 +87,22 @@
|
|||||||
v-model="instructors"
|
v-model="instructors"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
:label="__('Instructors')"
|
:label="__('Instructors')"
|
||||||
|
:required="true"
|
||||||
|
:filters="{ ignore_user_type: 1 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<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,14 @@
|
|||||||
<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 :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 +116,7 @@ const breadcrumbs = computed(() => {
|
|||||||
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
||||||
items.push({
|
items.push({
|
||||||
label: course?.data?.title,
|
label: course?.data?.title,
|
||||||
route: { name: 'CourseDetail', params: { course: course?.data?.name } },
|
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
@@ -131,26 +131,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,6 +57,11 @@
|
|||||||
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>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-xs text-gray-600 mb-2">
|
||||||
|
{{ __('Course Image') }}
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</div>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
v-if="!course.course_image"
|
v-if="!course.course_image"
|
||||||
:fileTypes="['image/*']"
|
:fileTypes="['image/*']"
|
||||||
@@ -50,40 +71,48 @@
|
|||||||
<template
|
<template
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||||
>
|
>
|
||||||
<div class="mb-4">
|
<div class="flex items-center">
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
<div class="border rounded-md w-fit py-5 px-20">
|
||||||
{{
|
<Image class="size-5 stroke-1 text-gray-700" />
|
||||||
uploading ? `Uploading ${progress}%` : 'Upload an image'
|
</div>
|
||||||
}}
|
<div class="ml-4">
|
||||||
|
<Button @click="openFileSelector">
|
||||||
|
{{ __('Upload') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<div class="mt-2 text-gray-600 text-sm">
|
||||||
|
{{
|
||||||
|
__('Appears on the course card in the course list')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else class="mb-4">
|
<div v-else class="mb-4">
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{{ __('Course Image') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="border rounded-md p-2 mr-2">
|
<img
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
:src="course.course_image.file_url"
|
||||||
</div>
|
class="border rounded-md w-40"
|
||||||
<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 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>
|
||||||
</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)) {
|
|
||||||
return __('Course price and currency are mandatory for paid courses')
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
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"
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="">
|
<div class="">
|
||||||
<Tabs
|
<Tabs
|
||||||
|
v-if="hasCourses"
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
||||||
:tabs="makeTabs"
|
:tabs="makeTabs"
|
||||||
@@ -101,18 +102,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 +167,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 +264,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,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const newJob = createResource({
|
|||||||
return {
|
return {
|
||||||
doc: {
|
doc: {
|
||||||
doctype: 'Job Opportunity',
|
doctype: 'Job Opportunity',
|
||||||
company_logo: job.image.file_url,
|
company_logo: job.image?.file_url,
|
||||||
...job,
|
...job,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,46 +52,88 @@
|
|||||||
</header>
|
</header>
|
||||||
<div v-if="job.data" class="max-w-3xl mx-auto">
|
<div v-if="job.data" class="max-w-3xl mx-auto">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="flex mb-10">
|
<div class="space-y-5 mb-10">
|
||||||
|
<div class="flex items-center">
|
||||||
<img
|
<img
|
||||||
:src="job.data.company_logo"
|
:src="job.data.company_logo"
|
||||||
class="w-16 h-16 rounded-lg object-contain mr-4"
|
class="w-16 h-16 rounded-lg object-contain mr-4"
|
||||||
:alt="job.data.company_name"
|
:alt="job.data.company_name"
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<div class="text-2xl font-semibold mb-4">
|
<div class="text-2xl font-semibold mb-4">
|
||||||
{{ job.data.job_title }}
|
{{ job.data.job_title }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-10 gap-y-2 md:gap-y-4"
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-10 gap-y-5 md:gap-y-5"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Building2 class="h-4 w-4 stroke-1.5" />
|
<span class="p-4 bg-green-50 rounded-full">
|
||||||
<span>{{ job.data.company_name }}</span>
|
<Building2 class="h-4 w-4 text-green-500" />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<span class="text-xs text-gray-600 font-medium uppercase">
|
||||||
|
{{ __('Organisation') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
|
{{ job.data.company_name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<MapPin class="h-4 w-4 stroke-1.5" />
|
<span class="p-4 bg-red-50 rounded-full">
|
||||||
<span>{{ job.data.location }}</span>
|
<MapPin class="h-4 w-4 text-red-500" />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<span class="text-xs text-gray-600 font-medium uppercase">
|
||||||
|
{{ __('Location') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
|
{{ job.data.location }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<ClipboardType class="h-4 w-4 stroke-1.5" />
|
<span class="p-4 bg-yellow-50 rounded-full">
|
||||||
<span>{{ job.data.type }}</span>
|
<ClipboardType class="h-4 w-4 text-yellow-500" />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<span class="text-xs font-medium text-gray-600 uppercase">
|
||||||
|
{{ __('Category') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
|
{{ job.data.type }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<CalendarDays class="h-4 w-4 stroke-1.5" />
|
<span class="p-4 bg-blue-50 rounded-full">
|
||||||
<span>
|
<CalendarDays class="h-4 w-4 text-blue-500" />
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<span class="text-xs text-gray-600 font-medium uppercase">
|
||||||
|
{{ __('Posted on') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
{{ dayjs(job.data.creation).format('DD MMM YYYY') }}
|
{{ dayjs(job.data.creation).format('DD MMM YYYY') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="applicationCount.data"
|
v-if="applicationCount.data"
|
||||||
class="flex items-center space-x-2"
|
class="flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
<SquareUserRound class="h-4 w-4 stroke-1.5" />
|
<span class="p-4 bg-purple-50 rounded-full">
|
||||||
<span
|
<SquareUserRound class="h-4 w-4 text-purple-500" />
|
||||||
>{{ applicationCount.data }}
|
</span>
|
||||||
{{ __('applications received') }}</span
|
<div class="flex flex-col space-y-2">
|
||||||
>
|
<span class="text-xs text-gray-600 font-medium uppercase">
|
||||||
|
{{ __('Applications Received') }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold">
|
||||||
|
{{ applicationCount.data }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,14 +17,9 @@
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<Button v-if="user.data" @click="enrollStudent()" variant="solid">
|
||||||
v-if="user.data"
|
|
||||||
:to="{ name: 'CourseDetail', params: { courseName: courseName } }"
|
|
||||||
>
|
|
||||||
<Button variant="solid">
|
|
||||||
{{ __('Start Learning') }}
|
{{ __('Start Learning') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
|
||||||
<Button v-else @click="redirectToLogin()">
|
<Button v-else @click="redirectToLogin()">
|
||||||
{{ __('Login') }}
|
{{ __('Login') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -120,6 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
|
lesson.data.instructor_content &&
|
||||||
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
|
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
|
||||||
allowInstructorContent()
|
allowInstructorContent()
|
||||||
"
|
"
|
||||||
@@ -193,7 +189,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'
|
||||||
@@ -203,6 +199,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)
|
||||||
@@ -242,6 +239,10 @@ const lesson = createResource({
|
|||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
router.push({ name: 'Courses' })
|
||||||
|
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 (
|
||||||
@@ -278,7 +279,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,14 +301,14 @@ const breadcrumbs = computed(() => {
|
|||||||
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
let items = [{ label: 'All Courses', route: { name: 'Courses' } }]
|
||||||
items.push({
|
items.push({
|
||||||
label: lesson?.data?.course_title,
|
label: lesson?.data?.course_title,
|
||||||
route: { name: 'CourseDetail', params: { course: props.courseName } },
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
})
|
})
|
||||||
items.push({
|
items.push({
|
||||||
label: lesson?.data?.title,
|
label: lesson?.data?.title,
|
||||||
route: {
|
route: {
|
||||||
name: 'Lesson',
|
name: 'Lesson',
|
||||||
params: {
|
params: {
|
||||||
course: props.courseName,
|
courseName: props.courseName,
|
||||||
chapterNumber: props.chapterNumber,
|
chapterNumber: props.chapterNumber,
|
||||||
lessonNumber: props.lessonNumber,
|
lessonNumber: props.lessonNumber,
|
||||||
},
|
},
|
||||||
@@ -378,6 +379,30 @@ const allowInstructorContent = () => {
|
|||||||
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}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,12 @@
|
|||||||
</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 +74,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,
|
||||||
|
|||||||
@@ -4,6 +4,19 @@
|
|||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
|
<router-link
|
||||||
|
v-if="quizDetails.data?.name"
|
||||||
|
:to="{
|
||||||
|
name: 'QuizPage',
|
||||||
|
params: {
|
||||||
|
quizID: quizDetails.data.name,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
{{ __('Open') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="quizDetails.data?.name"
|
v-if="quizDetails.data?.name"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -25,7 +38,7 @@
|
|||||||
<div class="w-3/4 mx-auto py-5">
|
<div class="w-3/4 mx-auto py-5">
|
||||||
<!-- Details -->
|
<!-- Details -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="text-sm font-semibold mb-4">
|
<div class="font-semibold mb-4">
|
||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -37,11 +50,17 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div v-if="quizDetails.data?.name">
|
<div v-if="quizDetails.data?.name">
|
||||||
<div class="grid grid-cols-3 gap-5 mt-4 mb-8">
|
<div class="grid grid-cols-2 gap-5 mt-4 mb-8">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
type="number"
|
||||||
v-model="quiz.max_attempts"
|
v-model="quiz.max_attempts"
|
||||||
:label="__('Maximun Attempts')"
|
:label="__('Maximun Attempts')"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="number"
|
||||||
|
v-model="quiz.duration"
|
||||||
|
:label="__('Duration (in minutes)')"
|
||||||
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="quiz.total_marks"
|
v-model="quiz.total_marks"
|
||||||
:label="__('Total Marks')"
|
:label="__('Total Marks')"
|
||||||
@@ -55,7 +74,7 @@
|
|||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="text-sm font-semibold mb-4">
|
<div class="font-semibold mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-5 my-4">
|
<div class="grid grid-cols-3 gap-5 my-4">
|
||||||
@@ -73,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="text-sm font-semibold mb-4">
|
<div class="font-semibold mb-4">
|
||||||
{{ __('Shuffle Settings') }}
|
{{ __('Shuffle Settings') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3">
|
<div class="grid grid-cols-3">
|
||||||
@@ -93,7 +112,7 @@
|
|||||||
<!-- Questions -->
|
<!-- Questions -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="text-sm font-semibold">
|
<div class="font-semibold">
|
||||||
{{ __('Questions') }}
|
{{ __('Questions') }}
|
||||||
</div>
|
</div>
|
||||||
<Button @click="openQuestionModal()">
|
<Button @click="openQuestionModal()">
|
||||||
@@ -213,6 +232,7 @@ const quiz = reactive({
|
|||||||
total_marks: 0,
|
total_marks: 0,
|
||||||
passing_percentage: 0,
|
passing_percentage: 0,
|
||||||
max_attempts: 0,
|
max_attempts: 0,
|
||||||
|
duration: 0,
|
||||||
limit_questions_to: 0,
|
limit_questions_to: 0,
|
||||||
show_answers: true,
|
show_answers: true,
|
||||||
show_submission_history: false,
|
show_submission_history: false,
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 ''
|
||||||
@@ -82,10 +91,13 @@ export function getFileSize(file_size) {
|
|||||||
|
|
||||||
export function showToast(title, text, icon, iconClasses = null) {
|
export function showToast(title, text, icon, iconClasses = null) {
|
||||||
if (!iconClasses) {
|
if (!iconClasses) {
|
||||||
iconClasses =
|
if (icon == 'check') {
|
||||||
icon == 'check'
|
iconClasses = 'bg-green-600 text-white rounded-md p-px'
|
||||||
? 'bg-green-600 text-white rounded-md p-px'
|
} else if (icon == 'circle-warn') {
|
||||||
: 'bg-red-600 text-white rounded-md p-px'
|
iconClasses = 'bg-yellow-600 text-white rounded-md p-px'
|
||||||
|
} else {
|
||||||
|
iconClasses = 'bg-red-600 text-white rounded-md p-px'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createToast({
|
createToast({
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -852,6 +852,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue-demi ">=0.14.8"
|
vue-demi ">=0.14.8"
|
||||||
|
|
||||||
|
ace-builds@^1.36.2:
|
||||||
|
version "1.36.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.36.2.tgz#9499bd59e839a335ac4850e74549ca8d849dc554"
|
||||||
|
integrity sha512-eqqfbGwx/GKjM/EnFu4QtQ+d2NNBu84MGgxoG8R5iyFpcVeQ4p9YlTL+ZzdEJqhdkASqoqOxCSNNGyB6lvMm+A==
|
||||||
|
|
||||||
ansi-regex@^5.0.1:
|
ansi-regex@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||||
@@ -1219,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.8.0"
|
__version__ = "2.11.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():
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSJobApplication(FrappeTestCase):
|
class TestLMSJobApplication(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
132
lms/lms/api.py
132
lms/lms/api.py
@@ -1,13 +1,15 @@
|
|||||||
"""API methods for the LMS.
|
"""API methods for the LMS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import frappe
|
import frappe
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -293,7 +295,11 @@ def get_branding():
|
|||||||
image_fields = ["banner_image", "footer_logo", "favicon"]
|
image_fields = ["banner_image", "footer_logo", "favicon"]
|
||||||
|
|
||||||
for field in image_fields:
|
for field in image_fields:
|
||||||
website_settings.update({field: get_file_info(website_settings.get(field))})
|
if website_settings.get(field):
|
||||||
|
file_info = get_file_info(website_settings.get(field))
|
||||||
|
website_settings.update({field: json.loads(json.dumps(file_info))})
|
||||||
|
else:
|
||||||
|
website_settings.update({field: None})
|
||||||
|
|
||||||
return website_settings
|
return website_settings
|
||||||
|
|
||||||
@@ -322,7 +328,7 @@ def get_evaluator_details(evaluator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if frappe.db.exists("Course Evaluator", {"evaluator": evaluator}):
|
if frappe.db.exists("Course Evaluator", {"evaluator": evaluator}):
|
||||||
doc = frappe.get_doc("Course Evaluator", evaluator, as_dict=1)
|
doc = frappe.get_doc("Course Evaluator", evaluator)
|
||||||
else:
|
else:
|
||||||
doc = frappe.new_doc("Course Evaluator")
|
doc = frappe.new_doc("Course Evaluator")
|
||||||
doc.evaluator = evaluator
|
doc.evaluator = evaluator
|
||||||
@@ -486,7 +492,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -576,14 +590,17 @@ def get_members(start=0, search=""):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
||||||
|
or_filters = {}
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
filters["full_name"] = ["like", f"%{search}%"]
|
or_filters["full_name"] = ["like", f"%{search}%"]
|
||||||
|
or_filters["email"] = ["like", f"%{search}%"]
|
||||||
|
|
||||||
members = frappe.get_all(
|
members = frappe.get_all(
|
||||||
"User",
|
"User",
|
||||||
filters=filters,
|
filters=filters,
|
||||||
fields=["name", "full_name", "user_image", "username", "last_active"],
|
fields=["name", "full_name", "user_image", "username", "last_active"],
|
||||||
|
or_filters=or_filters,
|
||||||
page_length=20,
|
page_length=20,
|
||||||
start=start,
|
start=start,
|
||||||
)
|
)
|
||||||
@@ -754,3 +771,108 @@ 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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestBatchStudent(FrappeTestCase):
|
class TestBatchStudent(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"course",
|
"course",
|
||||||
"title",
|
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"description",
|
"title",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"lessons"
|
"lessons"
|
||||||
],
|
],
|
||||||
@@ -35,11 +34,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"
|
||||||
@@ -59,7 +53,7 @@
|
|||||||
"link_fieldname": "chapter"
|
"link_fieldname": "chapter"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-09-29 17:03:58.013819",
|
"modified": "2024-10-29 16:54:20.904683",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Chapter",
|
"name": "Course Chapter",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestCourseEvaluator(FrappeTestCase):
|
class TestCourseEvaluator(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -15,20 +15,22 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Assessment Type",
|
"label": "Assessment Type",
|
||||||
"options": "DocType"
|
"options": "DocType",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "assessment_name",
|
"fieldname": "assessment_name",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Dynamic Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Assessment Name",
|
"label": "Assessment Name",
|
||||||
"options": "assessment_type"
|
"options": "assessment_type",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-29 14:56:36.602399",
|
"modified": "2024-10-11 19:16:01.630524",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Assessment",
|
"name": "LMS Assessment",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSAssignment(FrappeTestCase):
|
class TestLMSAssignment(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSBadge(FrappeTestCase):
|
class TestLMSBadge(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSBadgeAssignment(FrappeTestCase):
|
class TestLMSBadgeAssignment(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class LMSBatch(Document):
|
|||||||
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:
|
||||||
@@ -139,6 +140,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:
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSClass(FrappeTestCase):
|
class TestLMSBatch(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
class TestLMSBatchTimetable(FrappeTestCase):
|
class TestLMSBatchTimetable(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSCategory(FrappeTestCase):
|
class TestLMSCategory(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSCertificateEvaluation(FrappeTestCase):
|
class TestLMSCertificateEvaluation(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSCertificateRequest(FrappeTestCase):
|
class TestLMSCertificateRequest(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -187,192 +187,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}"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSLiveClass(FrappeTestCase):
|
class TestLMSLiveClass(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSPayment(FrappeTestCase):
|
class TestLMSPayment(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSQuestion(FrappeTestCase):
|
class TestLMSQuestion(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
"title",
|
"title",
|
||||||
"max_attempts",
|
"max_attempts",
|
||||||
"show_answers",
|
"show_answers",
|
||||||
|
"show_submission_history",
|
||||||
"column_break_gaac",
|
"column_break_gaac",
|
||||||
"total_marks",
|
"total_marks",
|
||||||
"passing_percentage",
|
"passing_percentage",
|
||||||
"show_submission_history",
|
"duration",
|
||||||
"section_break_tzbu",
|
"section_break_tzbu",
|
||||||
"shuffle_questions",
|
"shuffle_questions",
|
||||||
"column_break_clsh",
|
"column_break_clsh",
|
||||||
@@ -128,11 +129,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_clsh",
|
"fieldname": "column_break_clsh",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"label": "Duration"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-08-09 12:21:36.256522",
|
"modified": "2024-10-11 22:39:40.381183",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ 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:
|
||||||
|
if len(types) > 1:
|
||||||
frappe.throw(
|
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."
|
||||||
@@ -100,6 +101,16 @@ def quiz_summary(quiz, results):
|
|||||||
score = 0
|
score = 0
|
||||||
results = results and json.loads(results)
|
results = results and json.loads(results)
|
||||||
is_open_ended = False
|
is_open_ended = False
|
||||||
|
percentage = 0
|
||||||
|
|
||||||
|
quiz_details = frappe.db.get_value(
|
||||||
|
"LMS Quiz",
|
||||||
|
quiz,
|
||||||
|
["total_marks", "passing_percentage", "lesson", "course"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
score_out_of = quiz_details.total_marks
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value(
|
||||||
@@ -113,17 +124,6 @@ def quiz_summary(quiz, results):
|
|||||||
result["question"] = question_details.question_detail
|
result["question"] = question_details.question_detail
|
||||||
result["marks_out_of"] = question_details.marks
|
result["marks_out_of"] = question_details.marks
|
||||||
|
|
||||||
quiz_details = frappe.get_doc(
|
|
||||||
"LMS Quiz",
|
|
||||||
quiz,
|
|
||||||
["total_marks", "passing_percentage", "lesson", "course"],
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
score = 0
|
|
||||||
percentage = 0
|
|
||||||
score_out_of = quiz_details.total_marks
|
|
||||||
|
|
||||||
if question_details.type != "Open Ended":
|
if question_details.type != "Open Ended":
|
||||||
correct = result["is_correct"][0]
|
correct = result["is_correct"][0]
|
||||||
for point in result["is_correct"]:
|
for point in result["is_correct"]:
|
||||||
@@ -135,24 +135,26 @@ def quiz_summary(quiz, results):
|
|||||||
score += marks
|
score += marks
|
||||||
|
|
||||||
del result["question_name"]
|
del result["question_name"]
|
||||||
percentage = (score / score_out_of) * 100
|
|
||||||
else:
|
else:
|
||||||
result["is_correct"] = 0
|
result["is_correct"] = 0
|
||||||
is_open_ended = True
|
is_open_ended = True
|
||||||
|
|
||||||
|
percentage = (score / score_out_of) * 100
|
||||||
result["answer"] = re.sub(
|
result["answer"] = re.sub(
|
||||||
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
|
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
|
||||||
)
|
)
|
||||||
|
|
||||||
submission = frappe.get_doc(
|
submission = frappe.new_doc("LMS Quiz Submission")
|
||||||
|
# Score and percentage are calculated by the controller function
|
||||||
|
submission.update(
|
||||||
{
|
{
|
||||||
"doctype": "LMS Quiz Submission",
|
"doctype": "LMS Quiz Submission",
|
||||||
"quiz": quiz,
|
"quiz": quiz,
|
||||||
"result": results,
|
"result": results,
|
||||||
"score": score,
|
"score": 0,
|
||||||
"score_out_of": score_out_of,
|
"score_out_of": score_out_of,
|
||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
"percentage": percentage,
|
"percentage": 0,
|
||||||
"passing_percentage": quiz_details.passing_percentage,
|
"passing_percentage": quiz_details.passing_percentage,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSSidebarItem(FrappeTestCase):
|
class TestLMSSidebarItem(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSSource(FrappeTestCase):
|
class TestLMSSource(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSTimetableLegend(FrappeTestCase):
|
class TestLMSTimetableLegend(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLMSTimetableTemplate(FrappeTestCase):
|
class TestLMSTimetableTemplate(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestPaymentCountry(FrappeTestCase):
|
class TestPaymentCountry(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests import UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestZoomSettings(FrappeTestCase):
|
class TestZoomSettings(UnitTestCase):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ def get_payment_link(doctype, docname, title, amount, total_amount, currency, ad
|
|||||||
"redirect_to": redirect_to,
|
"redirect_to": redirect_to,
|
||||||
"payment": payment.name,
|
"payment": payment.name,
|
||||||
}
|
}
|
||||||
|
if payment_gateway == "Razorpay":
|
||||||
|
order = controller.create_order(**payment_details)
|
||||||
|
payment_details.update({"order_id": order.get("id")})
|
||||||
|
|
||||||
url = controller.get_payment_url(**payment_details)
|
url = controller.get_payment_url(**payment_details)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|||||||
@@ -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,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from frappe.utils import (
|
|||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
cstr,
|
cstr,
|
||||||
|
ceil,
|
||||||
flt,
|
flt,
|
||||||
fmt_money,
|
fmt_money,
|
||||||
format_date,
|
format_date,
|
||||||
@@ -108,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)
|
||||||
@@ -156,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)
|
||||||
@@ -169,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)
|
||||||
@@ -483,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",
|
||||||
@@ -948,7 +963,7 @@ def check_multicurrency(amount, currency, country=None, amount_usd=None):
|
|||||||
if apply_rounding and amount % 100 != 0:
|
if apply_rounding and amount % 100 != 0:
|
||||||
amount = amount + 100 - amount % 100
|
amount = amount + 100 - amount % 100
|
||||||
|
|
||||||
return amount, currency
|
return ceil(amount), currency
|
||||||
|
|
||||||
|
|
||||||
def apply_gst(amount, country=None):
|
def apply_gst(amount, country=None):
|
||||||
@@ -1026,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:
|
||||||
@@ -1091,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)
|
||||||
|
|
||||||
@@ -1123,7 +1128,7 @@ 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"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
chapter_details["idx"] = chapter.idx
|
chapter_details["idx"] = chapter.idx
|
||||||
@@ -1143,6 +1148,9 @@ def get_lesson(course, chapter, 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"], as_dict=1
|
||||||
)
|
)
|
||||||
|
if not lesson_details:
|
||||||
|
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 (
|
||||||
@@ -1257,7 +1265,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"
|
||||||
@@ -1677,7 +1685,7 @@ def update_payment_record(doctype, docname):
|
|||||||
if doctype == "LMS Course":
|
if doctype == "LMS Course":
|
||||||
enroll_in_course(data.payment, docname)
|
enroll_in_course(data.payment, docname)
|
||||||
else:
|
else:
|
||||||
enroll_in_batch(data.payment, docname)
|
enroll_in_batch(docname, data.payment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.log_error(frappe.get_traceback(), _("Enrollment Failed"))
|
frappe.log_error(frappe.get_traceback(), _("Enrollment Failed"))
|
||||||
|
|
||||||
@@ -1701,25 +1709,33 @@ def enroll_in_course(payment_name, course):
|
|||||||
enrollment.save(ignore_permissions=True)
|
enrollment.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
def enroll_in_batch(payment_name, batch):
|
@frappe.whitelist()
|
||||||
|
def enroll_in_batch(batch, payment_name=None):
|
||||||
if not frappe.db.exists(
|
if not frappe.db.exists(
|
||||||
"Batch Student", {"parent": batch, "student": frappe.session.user}
|
"Batch Student", {"parent": batch, "student": frappe.session.user}
|
||||||
):
|
):
|
||||||
student = frappe.new_doc("Batch Student")
|
student = frappe.new_doc("Batch Student")
|
||||||
current_count = frappe.db.count("Batch Student", {"parent": batch})
|
current_count = frappe.db.count("Batch Student", {"parent": batch})
|
||||||
payment = frappe.db.get_value(
|
|
||||||
"LMS Payment", payment_name, ["name", "source"], as_dict=True
|
|
||||||
)
|
|
||||||
|
|
||||||
student.update(
|
student.update(
|
||||||
{
|
{
|
||||||
"student": frappe.session.user,
|
"student": frappe.session.user,
|
||||||
"payment": payment.name,
|
|
||||||
"source": payment.source,
|
|
||||||
"parent": batch,
|
"parent": batch,
|
||||||
"parenttype": "LMS Batch",
|
"parenttype": "LMS Batch",
|
||||||
"parentfield": "students",
|
"parentfield": "students",
|
||||||
"idx": current_count + 1,
|
"idx": current_count + 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if payment_name:
|
||||||
|
payment = frappe.db.get_value(
|
||||||
|
"LMS Payment", payment_name, ["name", "source"], as_dict=True
|
||||||
|
)
|
||||||
|
student.update(
|
||||||
|
{
|
||||||
|
"payment": payment.name,
|
||||||
|
"source": payment.source,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
student.save(ignore_permissions=True)
|
student.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -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
|
|
||||||
5472
lms/locale/ar.po
Normal file
5472
lms/locale/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
5472
lms/locale/bs.po
Normal file
5472
lms/locale/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
5472
lms/locale/de.po
Normal file
5472
lms/locale/de.po
Normal file
File diff suppressed because it is too large
Load Diff
5472
lms/locale/eo.po
Normal file
5472
lms/locale/eo.po
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user