Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb82570ea | ||
|
|
e712d6ae42 | ||
|
|
6ffc953370 | ||
|
|
63bf6a5574 | ||
|
|
1e73fc5751 | ||
|
|
65604a0b88 | ||
|
|
5a1a39f5f5 | ||
|
|
d22576c85c | ||
|
|
b7e5332c38 | ||
|
|
ed8570fb88 | ||
|
|
ce69e6634d | ||
|
|
274db20c60 | ||
|
|
3d72072f1f | ||
|
|
ed156c09d7 | ||
|
|
fda3a1a468 | ||
|
|
c261387635 | ||
|
|
7a2fa4dae8 | ||
|
|
b0c41958d9 | ||
|
|
4f1dcbfb78 | ||
|
|
dc9ed099d0 | ||
|
|
95255d44a9 | ||
|
|
5a94e8df75 | ||
|
|
015e3f8490 | ||
|
|
558601f02b | ||
|
|
461d96a079 | ||
|
|
bacfaf4a71 | ||
|
|
0678def698 | ||
|
|
07b0a0af51 | ||
|
|
f12f6cb720 | ||
|
|
4e6c1478f9 | ||
|
|
f9fd36f77e | ||
|
|
db4c7424b3 | ||
|
|
9311043190 | ||
|
|
a8690e41e6 | ||
|
|
cda42b9ec5 | ||
|
|
21a75fdd6d | ||
|
|
a90a1e9855 | ||
|
|
2a046e2e8b | ||
|
|
bb41656d81 | ||
|
|
a88a107718 | ||
|
|
2d21469f91 | ||
|
|
960ebe4a79 | ||
|
|
46dba0c394 | ||
|
|
ba27e8ca95 | ||
|
|
30574ea0fd | ||
|
|
c3c985c4a1 | ||
|
|
7b3d2d8812 | ||
|
|
d573a9f008 | ||
|
|
85a05f56b2 | ||
|
|
904adfb905 | ||
|
|
b2201c29fd | ||
|
|
fe01f68623 | ||
|
|
531c8ebe94 | ||
|
|
52dfb5a360 | ||
|
|
7e04e7e461 | ||
|
|
bce47f606d | ||
|
|
4dc1fdfdd8 | ||
|
|
9a852b52bc | ||
|
|
71a57b1fc0 | ||
|
|
d634598db1 | ||
|
|
6377d682a4 | ||
|
|
6e1acfdc24 | ||
|
|
30ec1dfd7c | ||
|
|
3d209024dd | ||
|
|
9ce64a037d | ||
|
|
43117bc035 | ||
|
|
2af704043e | ||
|
|
fa14ffdcba | ||
|
|
492b715ea0 | ||
|
|
d452e20b8a | ||
|
|
6b634c15d9 | ||
|
|
eeaec3369f | ||
|
|
ce1eece90d | ||
|
|
030bff6592 | ||
|
|
65de46a59e | ||
|
|
974f67aefe | ||
|
|
e374ae3229 | ||
|
|
8b1058e577 | ||
|
|
aaa2eea5e6 | ||
|
|
54047e3c2c | ||
|
|
50fe94e47b | ||
|
|
6999f6641a | ||
|
|
c2b12aa65f | ||
|
|
1a731b6908 | ||
|
|
837d050628 | ||
|
|
8b00bec49c | ||
|
|
9ade643af0 | ||
|
|
a29b92a886 | ||
|
|
e2c28e211f | ||
|
|
65f5b6a0a4 | ||
|
|
75cea1ab78 | ||
|
|
5ab9131629 |
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://lms1:8000",
|
baseUrl: "http://testui:8000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe("Course Creation", () => {
|
|||||||
cy.visit("/lms/courses");
|
cy.visit("/lms/courses");
|
||||||
|
|
||||||
// Create a course
|
// Create a course
|
||||||
cy.get("header").children().last().children().last().click();
|
cy.get("button").contains("New").click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.url().should("include", "/courses/new/edit");
|
cy.url().should("include", "/courses/new/edit");
|
||||||
|
|
||||||
@@ -84,9 +84,8 @@ describe("Course Creation", () => {
|
|||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
|
|
||||||
cy.get("label").contains("Title").type("Test Lesson");
|
cy.get("label").contains("Title").type("Test Lesson");
|
||||||
|
|
||||||
cy.get("#content .ce-block").type(
|
cy.get("#content .ce-block").type(
|
||||||
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
"{enter}This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
||||||
);
|
);
|
||||||
cy.button("Save").click();
|
cy.button("Save").click();
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.csrf_token = '{{ csrf_token }}'
|
window.csrf_token = '{{ csrf_token }}'
|
||||||
|
window.setup_complete = '{{ setup_complete }}'
|
||||||
document.getElementById('seo-content').style.display = 'none';
|
document.getElementById('seo-content').style.display = 'none';
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@editorjs/paragraph": "^2.11.3",
|
"@editorjs/paragraph": "^2.11.3",
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
"@editorjs/simple-image": "^1.6.0",
|
||||||
"@editorjs/table": "^2.4.2",
|
"@editorjs/table": "^2.4.2",
|
||||||
|
"@vueuse/router": "^12.7.0",
|
||||||
"ace-builds": "^1.36.2",
|
"ace-builds": "^1.36.2",
|
||||||
"apexcharts": "^4.3.0",
|
"apexcharts": "^4.3.0",
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
|
|||||||
@@ -62,25 +62,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SidebarLink
|
<div>
|
||||||
:link="{
|
<TrialBanner
|
||||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
v-if="
|
||||||
}"
|
userResource.data?.user_type == 'System User' &&
|
||||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
userResource.data?.is_fc_site
|
||||||
@click="toggleSidebar()"
|
"
|
||||||
class="m-2"
|
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
>
|
/>
|
||||||
<template #icon>
|
<SidebarLink
|
||||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
:link="{
|
||||||
<CollapseSidebar
|
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||||
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
|
}"
|
||||||
:class="{
|
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
@click="toggleSidebar()"
|
||||||
}"
|
class="m-2"
|
||||||
/>
|
>
|
||||||
</span>
|
<template #icon>
|
||||||
</template>
|
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||||
</SidebarLink>
|
<CollapseSidebar
|
||||||
|
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
|
||||||
|
:class="{
|
||||||
|
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</SidebarLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PageModal
|
<PageModal
|
||||||
v-model="showPageModal"
|
v-model="showPageModal"
|
||||||
@@ -101,7 +110,7 @@ import { sessionStore } from '@/stores/session'
|
|||||||
import { useSidebar } from '@/stores/sidebar'
|
import { useSidebar } from '@/stores/sidebar'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import { ChevronRight, Plus } from 'lucide-vue-next'
|
import { ChevronRight, Plus } from 'lucide-vue-next'
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { Button, createResource, TrialBanner } from 'frappe-ui'
|
||||||
import PageModal from '@/components/Modals/PageModal.vue'
|
import PageModal from '@/components/Modals/PageModal.vue'
|
||||||
|
|
||||||
const { user, sidebarSettings } = sessionStore()
|
const { user, sidebarSettings } = sessionStore()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="assignment.data"
|
v-if="assignment.data"
|
||||||
class="grid grid-cols-[68%,32%] h-full"
|
class="grid grid-cols-[65%,35%] h-full"
|
||||||
:class="{ 'border rounded-lg': !showTitle }"
|
:class="{ 'border rounded-lg': !showTitle }"
|
||||||
>
|
>
|
||||||
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]">
|
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]">
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex items-center text-ink-gray-7">
|
<div class="flex text-ink-gray-7">
|
||||||
<div class="border rounded-md p-2 mr-2">
|
<div class="border self-start rounded-md p-2 mr-2">
|
||||||
<FileText class="h-5 w-5 stroke-1.5" />
|
<FileText class="h-5 w-5 stroke-1.5" />
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex flex-col cursor-pointer !no-underline"
|
class="flex flex-col cursor-pointer !no-underline"
|
||||||
>
|
>
|
||||||
<span>
|
<span class="text-sm leading-5">
|
||||||
{{ submissionFile.file_name }}
|
{{ submissionFile.file_name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-ink-gray-5 mt-1">
|
<span class="text-sm text-ink-gray-5 mt-1">
|
||||||
@@ -155,12 +155,23 @@
|
|||||||
type="select"
|
type="select"
|
||||||
:options="submissionStatusOptions"
|
:options="submissionStatusOptions"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<div>
|
||||||
v-if="submissionResource.doc"
|
<div class="text-sm text-ink-gray-5 mb-1">
|
||||||
v-model="submissionResource.doc.comments"
|
{{ __('Comments') }}
|
||||||
:label="__('Comments')"
|
</div>
|
||||||
type="textarea"
|
<TextEditor
|
||||||
/>
|
:content="comments"
|
||||||
|
@change="
|
||||||
|
(val) => {
|
||||||
|
comments = val
|
||||||
|
isDirty = true
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +195,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const submissionFile = ref(null)
|
const submissionFile = ref(null)
|
||||||
const answer = ref(null)
|
const answer = ref(null)
|
||||||
|
const comments = ref(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
|
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
|
||||||
@@ -281,6 +293,9 @@ watch(submissionResource, () => {
|
|||||||
if (submissionResource.doc.answer) {
|
if (submissionResource.doc.answer) {
|
||||||
answer.value = submissionResource.doc.answer
|
answer.value = submissionResource.doc.answer
|
||||||
}
|
}
|
||||||
|
if (submissionResource.doc.comments) {
|
||||||
|
comments.value = submissionResource.doc.comments
|
||||||
|
}
|
||||||
if (submissionResource.isDirty) {
|
if (submissionResource.isDirty) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
} else if (showUploader() && !submissionFile.value) {
|
} else if (showUploader() && !submissionFile.value) {
|
||||||
@@ -305,11 +320,14 @@ const submitAssignment = () => {
|
|||||||
submissionResource.doc && submissionResource.doc.owner != user.data?.name
|
submissionResource.doc && submissionResource.doc.owner != user.data?.name
|
||||||
? user.data?.name
|
? user.data?.name
|
||||||
: null
|
: null
|
||||||
|
|
||||||
submissionResource.setValue.submit(
|
submissionResource.setValue.submit(
|
||||||
{
|
{
|
||||||
...submissionResource.doc,
|
...submissionResource.doc,
|
||||||
assignment_attachment: submissionFile.value?.file_url,
|
assignment_attachment: submissionFile.value?.file_url,
|
||||||
evaluator: evaluator,
|
evaluator: evaluator,
|
||||||
|
comments: comments.value,
|
||||||
|
answer: answer.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div v-if="column.key == 'member_name'">
|
<div v-if="column.key == 'member_name'">
|
||||||
<Avatar
|
<Avatar
|
||||||
class="flex items-center"
|
class="flex"
|
||||||
:image="row['member_image']"
|
:image="row['member_image']"
|
||||||
:label="item"
|
:label="item"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -240,6 +240,6 @@ const feedbackColumns = computed(() => {
|
|||||||
<style>
|
<style>
|
||||||
.feedback-list > button > div {
|
.feedback-list > button > div {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 0.25rem 0;
|
padding: 0.15rem 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
||||||
<div
|
<div
|
||||||
v-if="batch.data.seat_count && seats_left > 0"
|
v-if="batch.data.seat_count && seats_left > 0"
|
||||||
class="text-xs bg-green-200 text-green-800 float-right px-2 py-0.5 rounded-md"
|
class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md"
|
||||||
>
|
>
|
||||||
{{ seats_left }}
|
{{ seats_left }}
|
||||||
<span v-if="seats_left > 1">
|
<span v-if="seats_left > 1">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="batch.data.seat_count && seats_left <= 0"
|
v-else-if="batch.data.seat_count && seats_left <= 0"
|
||||||
class="text-xs bg-red-200 text-red-900 float-right px-2 py-0.5 rounded-md"
|
class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
|
||||||
>
|
>
|
||||||
{{ __('Sold Out') }}
|
{{ __('Sold Out') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +69,11 @@
|
|||||||
name: batch.data.name,
|
name: batch.data.name,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
v-else-if="batch.data.paid_batch && batch.data.seats_left"
|
v-else-if="
|
||||||
|
batch.data.paid_batch &&
|
||||||
|
batch.data.seats_left > 0 &&
|
||||||
|
batch.data.accept_enrollments
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
|
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
|
||||||
<span>
|
<span>
|
||||||
@@ -80,7 +84,11 @@
|
|||||||
<Button
|
<Button
|
||||||
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 &&
|
||||||
|
batch.data.accept_enrollments
|
||||||
|
"
|
||||||
@click="enrollInBatch()"
|
@click="enrollInBatch()"
|
||||||
>
|
>
|
||||||
{{ __('Enroll Now') }}
|
{{ __('Enroll Now') }}
|
||||||
@@ -112,6 +120,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="chartData"
|
:series="chartData"
|
||||||
type="bar"
|
type="bar"
|
||||||
height="200"
|
:height="chartData[0].data.length * 30 + 100"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4"
|
class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4"
|
||||||
@@ -357,7 +357,7 @@ const getChartData = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Object.keys(student.assessments).forEach((assessment) => {
|
Object.keys(student.assessments).forEach((assessment) => {
|
||||||
if (student.assessments[assessment].result === 'Passed') {
|
if (student.assessments[assessment].result === 'Pass') {
|
||||||
categories[assessment].value += 1
|
categories[assessment].value += 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
67
frontend/src/components/CertificationLinks.vue
Normal file
67
frontend/src/components/CertificationLinks.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
certification.data &&
|
||||||
|
certification.data.membership &&
|
||||||
|
certification.data.paid_certificate &&
|
||||||
|
user.data?.is_student
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="!certification.data.membership.purchased_certificate"
|
||||||
|
:to="{
|
||||||
|
name: 'Billing',
|
||||||
|
params: {
|
||||||
|
type: 'certificate',
|
||||||
|
name: courseName,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<GraduationCap class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Get Certified') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-else-if="!certification.data.membership.certficate"
|
||||||
|
:to="{
|
||||||
|
name: 'CourseCertification',
|
||||||
|
params: {
|
||||||
|
courseName: courseName,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<GraduationCap class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Get Certified') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Button, createResource } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { GraduationCap } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const certification = createResource({
|
||||||
|
url: 'lms.lms.api.get_certification_details',
|
||||||
|
params: {
|
||||||
|
course: props.courseName,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: ['certificationData', user.data?.name],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -100,9 +100,15 @@
|
|||||||
<CourseInstructors :instructors="course.instructors" />
|
<CourseInstructors :instructors="course.instructors" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="font-semibold">
|
<div v-if="course.paid_course" class="font-semibold">
|
||||||
{{ course.price }}
|
{{ course.price }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="course.paid_certificate || course.enable_certification"
|
||||||
|
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
|
||||||
|
>
|
||||||
|
{{ __('Certification') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,30 +6,32 @@
|
|||||||
class="rounded-t-md min-h-56 w-full"
|
class="rounded-t-md min-h-56 w-full"
|
||||||
/>
|
/>
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div v-if="course.data.price" class="text-2xl font-semibold mb-3">
|
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
|
||||||
{{ course.data.price }}
|
{{ course.data.price }}
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<div v-if="course.data.membership" class="space-y-2">
|
||||||
v-if="course.data.membership"
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Lesson',
|
name: 'Lesson',
|
||||||
params: {
|
params: {
|
||||||
courseName: course.name,
|
courseName: course.name,
|
||||||
chapterNumber: course.data.current_lesson
|
chapterNumber: course.data.current_lesson
|
||||||
? course.data.current_lesson.split('-')[0]
|
? course.data.current_lesson.split('-')[0]
|
||||||
: 1,
|
: 1,
|
||||||
lessonNumber: course.data.current_lesson
|
lessonNumber: course.data.current_lesson
|
||||||
? course.data.current_lesson.split('-')[1]
|
? course.data.current_lesson.split('-')[1]
|
||||||
: 1,
|
: 1,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button variant="solid" size="md" class="w-full">
|
<Button variant="solid" size="md" class="w-full">
|
||||||
<span>
|
<span>
|
||||||
{{ __('Continue Learning') }}
|
{{ __('Continue Learning') }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<CertificationLinks :courseName="course.data.name" />
|
||||||
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-else-if="course.data.paid_course"
|
v-else-if="course.data.paid_course"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -113,17 +115,36 @@
|
|||||||
{{ course.data.rating }} {{ __('Rating') }}
|
{{ course.data.rating }} {{ __('Rating') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="course.data.enable_certification"
|
||||||
|
class="flex items-center font-semibold text-ink-gray-9"
|
||||||
|
>
|
||||||
|
<GraduationCap class="h-4 w-4 stroke-2" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ __('Certificate of Completion') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="course.data.paid_certificate"
|
||||||
|
class="flex items-center font-semibold text-ink-gray-9"
|
||||||
|
>
|
||||||
|
<GraduationCap class="h-4 w-4 stroke-2" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ __('Paid Certificate after Evaluation') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||||
import { showToast, formatAmount } 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'
|
||||||
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<div class="flex mt-2">
|
<div class="flex mt-2">
|
||||||
<Star
|
<Star
|
||||||
v-for="index in 5"
|
v-for="index in 5"
|
||||||
class="h-5 w-5 text-ink-gray-2 rounded-sm mr-2"
|
class="h-5 w-5 text-ink-gray-1 rounded-sm mr-2"
|
||||||
:class="
|
:class="
|
||||||
index <= Math.ceil(review.rating)
|
index <= Math.ceil(review.rating)
|
||||||
? 'fill-orange-500'
|
? 'fill-orange-500'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex space-x-4 border rounded-md p-2">
|
<div class="flex space-x-4 border rounded-md p-2">
|
||||||
<Avatar :image="job.company_logo" :label="job.job_title" size="2xl" />
|
<img :src="job.company_logo" class="size-10 rounded-full object-contain" />
|
||||||
<div class="flex flex-col space-y-2 flex-1">
|
<div class="flex flex-col space-y-2 flex-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold text-ink-gray-9">
|
<span class="font-semibold text-ink-gray-9">
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-model="member.first_name"
|
v-model="member.first_name"
|
||||||
:placeholder="__('First Name')"
|
:placeholder="__('First Name')"
|
||||||
type="test"
|
type="text"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<Button @click="addMember()" variant="subtle">
|
<Button @click="addMember()" variant="subtle">
|
||||||
|
|||||||
@@ -67,13 +67,16 @@ const announcement = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const announcementResource = createResource({
|
const announcementResource = createResource({
|
||||||
url: 'lms.lms.api.make_announcement',
|
url: 'frappe.core.doctype.communication.email.make',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
students: props.students,
|
recipients: props.students.join(', '),
|
||||||
cc: announcement.replyTo,
|
cc: announcement.replyTo,
|
||||||
subject: announcement.subject,
|
subject: announcement.subject,
|
||||||
content: announcement.announcement,
|
content: announcement.announcement,
|
||||||
|
doctype: 'LMS Batch',
|
||||||
|
name: props.batch,
|
||||||
|
send_email: 1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createResource } from 'frappe-ui'
|
import { Dialog, createResource } from 'frappe-ui'
|
||||||
import { ref, defineModel } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { showToast } from '@/utils'
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -32,25 +32,44 @@
|
|||||||
{{ __('Assessment') }}
|
{{ __('Assessment') }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ __('Progress') }}
|
{{ __('Percentage/Status') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<router-link
|
||||||
v-for="assessment in Object.keys(student.assessments)"
|
v-for="assessment in Object.keys(student.assessments)"
|
||||||
class="flex items-center text-ink-gray-7 font-medium"
|
class="flex items-center text-ink-gray-7 font-medium"
|
||||||
|
:to="{
|
||||||
|
name:
|
||||||
|
student.assessments[assessment].type == 'LMS Assignment'
|
||||||
|
? 'AssignmentSubmission'
|
||||||
|
: '',
|
||||||
|
params:
|
||||||
|
student.assessments[assessment].type == 'LMS Assignment'
|
||||||
|
? {
|
||||||
|
assignmentID:
|
||||||
|
student.assessments[assessment].assessment,
|
||||||
|
submissionName:
|
||||||
|
student.assessments[assessment].submission,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<span class="flex-1">
|
<span class="flex-1">
|
||||||
{{ assessment }}
|
{{ assessment }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="isAssignment(student.assessments[assessment])">
|
<span v-if="isAssignment(student.assessments[assessment].status)">
|
||||||
<Badge :theme="getStatusTheme(student.assessments[assessment])">
|
<Badge
|
||||||
{{ student.assessments[assessment] }}
|
:theme="
|
||||||
|
getStatusTheme(student.assessments[assessment].status)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ student.assessments[assessment].status }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ student.assessments[assessment] }}
|
{{ student.assessments[assessment].status }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Courses -->
|
<!-- Courses -->
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch } from 'vue'
|
import { reactive, watch } from 'vue'
|
||||||
import { showToast, getFileSize } from '@/utils/'
|
import { showToast, getFileSize } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
||||||
import { reactive, defineModel } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { showToast, singularize } from '@/utils'
|
import { showToast, singularize } from '@/utils'
|
||||||
|
|
||||||
const topics = defineModel('reloadTopics')
|
const topics = defineModel('reloadTopics')
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, watch, defineModel } from 'vue'
|
import { reactive, watch } from 'vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
import { getFileSize, showToast, escapeHTML } from '@/utils'
|
import { getFileSize, showToast, escapeHTML } from '@/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,15 @@
|
|||||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
{{ __('Date') }}
|
{{ __('Date') }}
|
||||||
</div>
|
</div>
|
||||||
<FormControl type="date" v-model="evaluation.date" />
|
<FormControl
|
||||||
|
type="date"
|
||||||
|
v-model="evaluation.date"
|
||||||
|
:min="
|
||||||
|
dayjs()
|
||||||
|
.add(dayjs.duration({ days: 1 }))
|
||||||
|
.format('YYYY-MM-DD')
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slots.data?.length">
|
<div v-if="slots.data?.length">
|
||||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
@@ -58,7 +66,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
|
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch, inject } from 'vue'
|
import { reactive, watch, inject } from 'vue'
|
||||||
import { createToast, formatTime } from '@/utils/'
|
import { createToast, formatTime } from '@/utils/'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -161,6 +169,11 @@ const getCourses = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (courses.length == 1) {
|
||||||
|
evaluation.course = courses[0].value
|
||||||
|
}
|
||||||
|
|
||||||
return courses
|
return courses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
94
frontend/src/components/Modals/FCVerfiyCodeModal.vue
Normal file
94
frontend/src/components/Modals/FCVerfiyCodeModal.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
size: 'xl',
|
||||||
|
title: __('Login to Frappe Cloud'),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Verify'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: (close) => {
|
||||||
|
verifyCode(close)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{{ __('We have sent the verificaton code to your email id ') }}
|
||||||
|
<b>{{ props.email }}</b>
|
||||||
|
</p>
|
||||||
|
<FormControl
|
||||||
|
v-model="code"
|
||||||
|
:label="__('Verification Code')"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
{{ __("Didn't receive the code?") }}
|
||||||
|
<a href="#" @click="resendCode">{{ __('Resend') }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { call, Dialog } from 'frappe-ui'
|
||||||
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const code = ref('')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const verifyCode = (close) => {
|
||||||
|
if (!code.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
call(
|
||||||
|
'frappe.integrations.frappe_providers.frappecloud_billing.verify_verification_code',
|
||||||
|
{
|
||||||
|
verification_code: code.value,
|
||||||
|
route: window.route,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.message.login_token) {
|
||||||
|
close()
|
||||||
|
window.open(
|
||||||
|
`${frappeCloudBaseEndpoint}/api/method/press.api.developer.saas.login_to_fc?token=${data.message.login_token}`,
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
showToast(
|
||||||
|
__('Frappe Cloud Login Successful'),
|
||||||
|
`<p>${__('You will be redirected to Frappe Cloud soon.')}</p><p>${__(
|
||||||
|
"If you haven't been redirected,"
|
||||||
|
)} <a href="${frappeCloudBaseEndpoint}/api/method/press.api.developer.saas.login_to_fc?token=${
|
||||||
|
data.message.login_token
|
||||||
|
}" target="_blank">${__('Click here to login')}</a></p>`,
|
||||||
|
'check'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showToast(__('Login failed'), __('Please try again'), 'x')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
showToast(__('Login failed'), __('Please try again'), 'x')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendCode = () => {
|
||||||
|
call(
|
||||||
|
'frappe.integrations.frappe_providers.frappecloud_billing.send_verification_code'
|
||||||
|
).catch((err) => {
|
||||||
|
showToast(__('Failed to resend code'), __('Please try again'), 'x')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
|
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
|
||||||
import { FileText } from 'lucide-vue-next'
|
import { FileText } from 'lucide-vue-next'
|
||||||
import { ref, inject, defineModel } from 'vue'
|
import { ref, inject } from 'vue'
|
||||||
import { createToast, getFileSize } from '@/utils/'
|
import { createToast, getFileSize } from '@/utils/'
|
||||||
|
|
||||||
const resume = ref(null)
|
const resume = ref(null)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
||||||
import { defineModel, reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import Rating from '@/components/Controls/Rating.vue'
|
import Rating from '@/components/Controls/Rating.vue'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast } from '@/utils/'
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="activeQuestion != questions.length"
|
v-else-if="activeQuestion != questions.length"
|
||||||
@click="nextQuetion()"
|
@click="nextQuestion()"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ __('Next') }}
|
{{ __('Next') }}
|
||||||
@@ -258,14 +258,22 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="quiz.data.show_submission_history && attempts?.data"
|
v-if="
|
||||||
|
quiz.data.show_submission_history &&
|
||||||
|
attempts?.data &&
|
||||||
|
attempts.data.length > 0
|
||||||
|
"
|
||||||
class="mt-10"
|
class="mt-10"
|
||||||
>
|
>
|
||||||
<ListView
|
<ListView
|
||||||
:columns="getSubmissionColumns()"
|
:columns="getSubmissionColumns()"
|
||||||
:rows="attempts?.data"
|
:rows="attempts?.data"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{ selectable: false, showTooltip: false }"
|
:options="{
|
||||||
|
selectable: false,
|
||||||
|
showTooltip: false,
|
||||||
|
emptyState: { title: __('No Quiz submissions found') },
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +290,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, watch, reactive, inject, computed } from 'vue'
|
import { ref, watch, reactive, inject, computed } from 'vue'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast, showToast } 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 { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -536,7 +544,7 @@ const addToLocalStorage = () => {
|
|||||||
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
|
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextQuetion = () => {
|
const nextQuestion = () => {
|
||||||
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
|
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
|
||||||
checkAnswer()
|
checkAnswer()
|
||||||
} else {
|
} else {
|
||||||
@@ -574,6 +582,16 @@ const createSubmission = () => {
|
|||||||
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
||||||
if (quiz.data.duration) clearInterval(timerInterval)
|
if (quiz.data.duration) clearInterval(timerInterval)
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
const errorTitle = err?.message || ''
|
||||||
|
if (errorTitle.includes('MaximumAttemptsExceededError')) {
|
||||||
|
const errorMessage = err.messages?.[0] || err
|
||||||
|
showToast(__('Error'), __(errorMessage), 'x')
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
<div class="text-lg font-semibold">
|
<div class="text-lg font-semibold">
|
||||||
{{ __('Upcoming Evaluations') }}
|
{{ __('Upcoming Evaluations') }}
|
||||||
</div>
|
</div>
|
||||||
<Button @click="openEvalModal">
|
<Button
|
||||||
|
v-if="
|
||||||
|
!upcoming_evals.data?.length ||
|
||||||
|
upcoming_evals.length == courses.length
|
||||||
|
"
|
||||||
|
@click="openEvalModal"
|
||||||
|
>
|
||||||
{{ __('Schedule Evaluation') }}
|
{{ __('Schedule Evaluation') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="upcoming_evals.data?.length">
|
<div v-if="upcoming_evals.data?.length">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div v-for="evl in upcoming_evals.data">
|
<div v-for="evl in upcoming_evals.data">
|
||||||
<div class="border rounded-md p-3">
|
<div class="border rounded-md p-3">
|
||||||
<div class="font-semibold mb-3">
|
<div class="font-semibold mb-3">
|
||||||
@@ -28,17 +34,39 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<UserCog2 class="w-4 h-4 stroke-1.5" />
|
<GraduationCap class="w-4 h-4 stroke-1.5" />
|
||||||
<span class="ml-2 font-medium">
|
<span class="ml-2">
|
||||||
{{ evl.evaluator_name }}
|
{{ evl.evaluator_name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between space-x-2 mt-4">
|
||||||
|
<Button
|
||||||
|
v-if="evl.google_meet_link"
|
||||||
|
@click="openEvalCall(evl)"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<HeadsetIcon class="w-4 h-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Join Call') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="evl.date > dayjs().format()"
|
||||||
|
@click="cancelEvaluation(evl)"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Ban class="w-4 h-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-ink-gray-5">
|
<div v-else class="text-sm italic text-ink-gray-5">
|
||||||
{{ __('No upcoming evaluations.') }}
|
{{ __('Please schedule an evaluation to get certified.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EvaluationModal
|
<EvaluationModal
|
||||||
@@ -50,15 +78,23 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
|
import {
|
||||||
import { inject, ref } from 'vue'
|
Ban,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
GraduationCap,
|
||||||
|
HeadsetIcon,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { inject, ref, getCurrentInstance } from 'vue'
|
||||||
import { formatTime } from '../utils'
|
import { formatTime } from '../utils'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource, call } from 'frappe-ui'
|
||||||
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showEvalModal = ref(false)
|
const showEvalModal = ref(false)
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
@@ -77,10 +113,10 @@ const props = defineProps({
|
|||||||
|
|
||||||
const upcoming_evals = createResource({
|
const upcoming_evals = createResource({
|
||||||
url: 'lms.lms.utils.get_upcoming_evals',
|
url: 'lms.lms.utils.get_upcoming_evals',
|
||||||
cache: ['upcoming_evals', user.data.name],
|
|
||||||
params: {
|
params: {
|
||||||
student: user.data.name,
|
student: user.data.name,
|
||||||
courses: props.courses.map((course) => course.course),
|
courses: props.courses.map((course) => course.course),
|
||||||
|
batch: props.batch,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
@@ -88,4 +124,32 @@ const upcoming_evals = createResource({
|
|||||||
function openEvalModal() {
|
function openEvalModal() {
|
||||||
showEvalModal.value = true
|
showEvalModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openEvalCall = (evl) => {
|
||||||
|
window.open(evl.google_meet_link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEvaluation = (evl) => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Cancel this evaluation?'),
|
||||||
|
message: __(
|
||||||
|
'Are you sure you want to cancel this evaluation? This action cannot be undone.'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Cancel'),
|
||||||
|
theme: 'red',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(close) {
|
||||||
|
call('lms.lms.api.cancel_evaluation', { evaluation: evl }).then(
|
||||||
|
() => {
|
||||||
|
upcoming_evals.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -59,13 +59,22 @@
|
|||||||
v-if="userResource.data?.is_moderator"
|
v-if="userResource.data?.is_moderator"
|
||||||
v-model="showSettingsModal"
|
v-model="showSettingsModal"
|
||||||
/>
|
/>
|
||||||
|
<FCVerfiyCodeModal v-if="showFCLoginDialog" :email="verificationEmail" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { call, Dropdown } from 'frappe-ui'
|
||||||
import Apps from '@/components/Apps.vue'
|
import Apps from '@/components/Apps.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { convertToTitleCase, showToast } from '@/utils'
|
||||||
|
import { usersStore } from '@/stores/user'
|
||||||
|
import { useSettings } from '@/stores/settings'
|
||||||
|
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
||||||
|
import SettingsModal from '@/components/Modals/Settings.vue'
|
||||||
|
import { createDialog } from '@/utils/dialogs'
|
||||||
|
import FCVerfiyCodeModal from './Modals/FCVerfiyCodeModal.vue'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LogIn,
|
LogIn,
|
||||||
@@ -74,13 +83,8 @@ import {
|
|||||||
User,
|
User,
|
||||||
Settings,
|
Settings,
|
||||||
Sun,
|
Sun,
|
||||||
|
LogInIcon,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { convertToTitleCase } from '../utils'
|
|
||||||
import { usersStore } from '@/stores/user'
|
|
||||||
import { useSettings } from '@/stores/settings'
|
|
||||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
|
||||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { logout, branding } = sessionStore()
|
const { logout, branding } = sessionStore()
|
||||||
@@ -89,6 +93,11 @@ const settingsStore = useSettings()
|
|||||||
let { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
const showSettingsModal = ref(false)
|
const showSettingsModal = ref(false)
|
||||||
const theme = ref('light')
|
const theme = ref('light')
|
||||||
|
const $dialog = createDialog
|
||||||
|
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
|
||||||
|
|
||||||
|
const showFCLoginDialog = ref(false)
|
||||||
|
const verificationEmail = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isCollapsed: {
|
isCollapsed: {
|
||||||
@@ -130,6 +139,13 @@ const userDropdownOptions = computed(() => {
|
|||||||
return isLoggedIn
|
return isLoggedIn
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: theme.value === 'light' ? Moon : Sun,
|
||||||
|
label: 'Toggle Theme',
|
||||||
|
onClick: () => {
|
||||||
|
toggleTheme()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: markRaw(Apps),
|
component: markRaw(Apps),
|
||||||
condition: () => {
|
condition: () => {
|
||||||
@@ -139,13 +155,6 @@ const userDropdownOptions = computed(() => {
|
|||||||
else return false
|
else return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: theme.value === 'light' ? Moon : Sun,
|
|
||||||
label: 'Toggle Theme',
|
|
||||||
onClick: () => {
|
|
||||||
toggleTheme()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
@@ -156,6 +165,19 @@ const userDropdownOptions = computed(() => {
|
|||||||
return userResource.data?.is_moderator
|
return userResource.data?.is_moderator
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: LogInIcon,
|
||||||
|
label: 'Login to Frappe Cloud',
|
||||||
|
onClick: () => {
|
||||||
|
initiateRequestForLoginToFrappeCloud()
|
||||||
|
},
|
||||||
|
condition: () => {
|
||||||
|
return (
|
||||||
|
userResource.data?.user_type == 'System User' &&
|
||||||
|
userResource.data?.is_fc_site
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: LogOut,
|
icon: LogOut,
|
||||||
label: 'Log out',
|
label: 'Log out',
|
||||||
@@ -180,4 +202,48 @@ const userDropdownOptions = computed(() => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initiateRequestForLoginToFrappeCloud = () => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Login to Frappe Cloud?'),
|
||||||
|
message: __(
|
||||||
|
'Are you sure you want to login to your Frappe Cloud dashboard?'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Confirm'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(close) {
|
||||||
|
requestLoginToFC()
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestLoginToFC = () => {
|
||||||
|
call(
|
||||||
|
'frappe.integrations.frappe_providers.frappecloud_billing.send_verification_code'
|
||||||
|
)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.message.is_user_logged_in) {
|
||||||
|
window.open(
|
||||||
|
`${frappeCloudBaseEndpoint}${data.message.redirect_to}`,
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
showFCLoginDialog.value = true
|
||||||
|
verificationEmail.value = data.message.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
showToast(
|
||||||
|
__('Failed to login to Frappe Cloud'),
|
||||||
|
__('Please try again'),
|
||||||
|
'x'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="user.data?.is_moderator"
|
v-if="user.data?.is_moderator && batch.data?.certification"
|
||||||
@click="openCertificateDialog = true"
|
@click="openCertificateDialog = true"
|
||||||
>
|
>
|
||||||
{{ __('Generate Certificates') }}
|
{{ __('Generate Certificates') }}
|
||||||
@@ -21,7 +21,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="batch.data" class="grid grid-cols-[75%,25%]">
|
<div
|
||||||
|
v-if="batch.data"
|
||||||
|
class="grid grid-cols-[75%,25%] h-[calc(100vh-3.2rem)]"
|
||||||
|
>
|
||||||
<div class="border-r">
|
<div class="border-r">
|
||||||
<Tabs
|
<Tabs
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
@@ -190,8 +193,9 @@
|
|||||||
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" />
|
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
|
||||||
import { computed, inject, ref } from 'vue'
|
import { computed, inject, ref } from 'vue'
|
||||||
|
import { useRouteQuery } from '@vueuse/router'
|
||||||
|
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import {
|
import {
|
||||||
@@ -267,7 +271,7 @@ const isStudent = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabIndex = ref(0)
|
const tabIndex = useRouteQuery('tab', 0)
|
||||||
const tabs = computed(() => {
|
const tabs = computed(() => {
|
||||||
let batchTabs = []
|
let batchTabs = []
|
||||||
batchTabs.push({
|
batchTabs.push({
|
||||||
@@ -310,7 +314,7 @@ const tabs = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const redirectToLogin = () => {
|
const redirectToLogin = () => {
|
||||||
window.location.href = `/login?redirect-to=/batches`
|
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAnnouncementModal = () => {
|
const openAnnouncementModal = () => {
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ const batch = createResource({
|
|||||||
batch: props.batchName,
|
batch: props.batchName,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (!data) {
|
||||||
|
router.push({ name: 'Batches' })
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const courses = createResource({
|
const courses = createResource({
|
||||||
|
|||||||
@@ -13,15 +13,14 @@
|
|||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
<div class="space-y-4 mb-4">
|
||||||
<div>
|
<FormControl
|
||||||
<FormControl
|
v-model="batch.title"
|
||||||
v-model="batch.title"
|
:label="__('Title')"
|
||||||
:label="__('Title')"
|
:required="true"
|
||||||
:required="true"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div class="flex items-center space-x-5">
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.published"
|
v-model="batch.published"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -32,6 +31,11 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:label="__('Allow self enrollment')"
|
:label="__('Allow self enrollment')"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.certification"
|
||||||
|
type="checkbox"
|
||||||
|
:label="__('Certification')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,30 +94,8 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
:filters="{ ignore_user_type: 1 }"
|
:filters="{ ignore_user_type: 1 }"
|
||||||
/>
|
/>
|
||||||
<div class="mb-4">
|
|
||||||
<FormControl
|
<div class="my-10">
|
||||||
v-model="batch.description"
|
|
||||||
:label="__('Description')"
|
|
||||||
type="textarea"
|
|
||||||
class="my-4"
|
|
||||||
:placeholder="__('Short description of the batch')"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-ink-gray-5 mb-1">
|
|
||||||
{{ __('Batch Details') }}
|
|
||||||
<span class="text-ink-red-3">*</span>
|
|
||||||
</label>
|
|
||||||
<TextEditor
|
|
||||||
:content="batch.batch_details"
|
|
||||||
@change="(val) => (batch.batch_details = val)"
|
|
||||||
:editable="true"
|
|
||||||
:fixedMenu="true"
|
|
||||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Date and Time') }}
|
{{ __('Date and Time') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +115,14 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.timezone"
|
||||||
|
:label="__('Timezone')"
|
||||||
|
type="text"
|
||||||
|
:placeholder="__('Example: IST (+5:30)')"
|
||||||
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -149,18 +139,11 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
v-model="batch.timezone"
|
|
||||||
:label="__('Timezone')"
|
|
||||||
type="text"
|
|
||||||
:placeholder="__('Example: IST (+5:30)')"
|
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
|
<div class="mb-10">
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -179,6 +162,11 @@
|
|||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="Email Template"
|
||||||
|
:label="__('Email Template')"
|
||||||
|
v-model="batch.confirmation_email_template"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -230,6 +218,33 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="my-10">
|
||||||
|
<div class="text-lg font-semibold mb-4">
|
||||||
|
{{ __('Description') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.description"
|
||||||
|
:label="__('Short Description')"
|
||||||
|
type="textarea"
|
||||||
|
class="my-4"
|
||||||
|
:placeholder="__('Short description of the batch')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-ink-gray-5 mb-1">
|
||||||
|
{{ __('Batch Details') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor
|
||||||
|
:content="batch.batch_details"
|
||||||
|
@change="(val) => (batch.batch_details = val)"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -278,10 +293,12 @@ const batch = reactive({
|
|||||||
end_time: '',
|
end_time: '',
|
||||||
timezone: '',
|
timezone: '',
|
||||||
evaluation_end_date: '',
|
evaluation_end_date: '',
|
||||||
|
confirmation_email_template: '',
|
||||||
seat_count: '',
|
seat_count: '',
|
||||||
medium: '',
|
medium: '',
|
||||||
category: '',
|
category: '',
|
||||||
allow_self_enrollment: false,
|
allow_self_enrollment: false,
|
||||||
|
certification: false,
|
||||||
image: null,
|
image: null,
|
||||||
paid_batch: false,
|
paid_batch: false,
|
||||||
currency: '',
|
currency: '',
|
||||||
@@ -351,7 +368,12 @@ const batchDetail = createResource({
|
|||||||
batch[key] = `${hours}:${minutes}`
|
batch[key] = `${hours}:${minutes}`
|
||||||
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
|
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
|
||||||
})
|
})
|
||||||
let checkboxes = ['published', 'paid_batch', 'allow_self_enrollment']
|
let checkboxes = [
|
||||||
|
'published',
|
||||||
|
'paid_batch',
|
||||||
|
'allow_self_enrollment',
|
||||||
|
'certification',
|
||||||
|
]
|
||||||
for (let idx in checkboxes) {
|
for (let idx in checkboxes) {
|
||||||
let key = checkboxes[idx]
|
let key = checkboxes[idx]
|
||||||
batch[key] = batch[key] ? true : false
|
batch[key] = batch[key] ? true : false
|
||||||
|
|||||||
@@ -26,13 +26,19 @@
|
|||||||
{{ __('All Batches') }}
|
{{ __('All Batches') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-2"
|
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
|
||||||
>
|
>
|
||||||
<TabButtons
|
<TabButtons
|
||||||
v-if="user.data"
|
v-if="user.data"
|
||||||
:buttons="batchTabs"
|
:buttons="batchTabs"
|
||||||
v-model="currentTab"
|
v-model="currentTab"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="certification"
|
||||||
|
:label="__('Certification')"
|
||||||
|
type="checkbox"
|
||||||
|
@change="updateBatches()"
|
||||||
|
/>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="title"
|
v-model="title"
|
||||||
@@ -111,6 +117,7 @@ const pageLength = ref(20)
|
|||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
|
const certification = ref(false)
|
||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
|
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
|
||||||
const orderBy = ref('start_date')
|
const orderBy = ref('start_date')
|
||||||
@@ -130,6 +137,7 @@ const setFiltersFromQuery = () => {
|
|||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
title.value = queries.get('title') || ''
|
title.value = queries.get('title') || ''
|
||||||
currentCategory.value = queries.get('category') || null
|
currentCategory.value = queries.get('category') || null
|
||||||
|
certification.value = queries.get('certification') || false
|
||||||
}
|
}
|
||||||
|
|
||||||
const batches = createListResource({
|
const batches = createListResource({
|
||||||
@@ -161,6 +169,7 @@ const updateBatches = () => {
|
|||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
updateCategoryFilter()
|
updateCategoryFilter()
|
||||||
updateTitleFilter()
|
updateTitleFilter()
|
||||||
|
updateCertificationFilter()
|
||||||
updateTabFilter()
|
updateTabFilter()
|
||||||
updateStudentFilter()
|
updateStudentFilter()
|
||||||
setQueryParams()
|
setQueryParams()
|
||||||
@@ -182,6 +191,14 @@ const updateTitleFilter = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateCertificationFilter = () => {
|
||||||
|
if (certification.value) {
|
||||||
|
filters.value['certification'] = 1
|
||||||
|
} else {
|
||||||
|
delete filters.value['certification']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateTabFilter = () => {
|
const updateTabFilter = () => {
|
||||||
orderBy.value = 'start_date'
|
orderBy.value = 'start_date'
|
||||||
if (!user.data) {
|
if (!user.data) {
|
||||||
@@ -222,6 +239,7 @@ const setQueryParams = () => {
|
|||||||
let filterKeys = {
|
let filterKeys = {
|
||||||
title: title.value,
|
title: title.value,
|
||||||
category: currentCategory.value,
|
category: currentCategory.value,
|
||||||
|
certification: certification.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(filterKeys).forEach((key) => {
|
Object.keys(filterKeys).forEach((key) => {
|
||||||
|
|||||||
@@ -12,20 +12,15 @@
|
|||||||
v-if="access.data?.access && orderSummary.data"
|
v-if="access.data?.access && orderSummary.data"
|
||||||
class="pt-5 pb-10 mx-5"
|
class="pt-5 pb-10 mx-5"
|
||||||
>
|
>
|
||||||
<!-- <div class="mb-5">
|
|
||||||
<div class="text-lg font-semibold">
|
|
||||||
{{ __('Address') }}
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<div class="flex flex-col lg:flex-row justify-between">
|
<div class="flex flex-col lg:flex-row justify-between">
|
||||||
<div
|
<div
|
||||||
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 text-sm font-medium lg:w-1/4"
|
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 font-medium lg:w-1/3"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between space-x-2">
|
<div class="flex items-baseline justify-between space-y-2">
|
||||||
<div class="text-ink-gray-5">
|
<div class="text-ink-gray-5">
|
||||||
{{ __('Ordered Item') }}
|
{{ __('Payment for ') }} {{ type }}:
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="leading-5">
|
||||||
{{ orderSummary.data.title }}
|
{{ orderSummary.data.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +121,7 @@
|
|||||||
<p class="text-ink-gray-5">
|
<p class="text-ink-gray-5">
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
'Make sure to enter the right billing name as the same will be used in your invoice.'
|
'Make sure to enter the correct billing name as the same will be used in your invoice.'
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -140,10 +135,10 @@
|
|||||||
<div v-else-if="access.data?.message">
|
<div v-else-if="access.data?.message">
|
||||||
<NotPermitted
|
<NotPermitted
|
||||||
:text="access.data.message"
|
:text="access.data.message"
|
||||||
:buttonLabel="
|
:buttonLabel="type == 'course' ? 'Checkout Course' : 'Checkout Batch'"
|
||||||
type == 'course' ? 'Checkout Courses' : 'Checkout Batches'
|
:buttonLink="
|
||||||
|
type == 'course' ? `/lms/courses/${name}` : `/lms/batches/${name}`
|
||||||
"
|
"
|
||||||
:buttonLink="type == 'course' ? '/lms/courses' : '/lms/batches'"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!user.data?.name">
|
<div v-else-if="!user.data?.name">
|
||||||
@@ -163,7 +158,7 @@ import {
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, inject, onMounted, ref } from 'vue'
|
import { reactive, inject, onMounted, computed } from 'vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import NotPermitted from '@/components/NotPermitted.vue'
|
import NotPermitted from '@/components/NotPermitted.vue'
|
||||||
import { showToast } from '@/utils/'
|
import { showToast } from '@/utils/'
|
||||||
@@ -193,7 +188,7 @@ const props = defineProps({
|
|||||||
const access = createResource({
|
const access = createResource({
|
||||||
url: 'lms.lms.api.validate_billing_access',
|
url: 'lms.lms.api.validate_billing_access',
|
||||||
params: {
|
params: {
|
||||||
type: props.type,
|
billing_type: props.type,
|
||||||
name: props.name,
|
name: props.name,
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
@@ -206,7 +201,7 @@ const orderSummary = createResource({
|
|||||||
url: 'lms.lms.utils.get_order_summary',
|
url: 'lms.lms.utils.get_order_summary',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||||
docname: props.name,
|
docname: props.name,
|
||||||
country: billingDetails.country,
|
country: billingDetails.country,
|
||||||
}
|
}
|
||||||
@@ -236,22 +231,26 @@ const paymentLink = createResource({
|
|||||||
url: 'lms.lms.payments.get_payment_link',
|
url: 'lms.lms.payments.get_payment_link',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||||
docname: props.name,
|
docname: props.name,
|
||||||
title: orderSummary.data.title,
|
title: orderSummary.data.title,
|
||||||
amount: orderSummary.data.original_amount,
|
amount: orderSummary.data.original_amount,
|
||||||
total_amount: orderSummary.data.amount,
|
total_amount: orderSummary.data.amount,
|
||||||
currency: orderSummary.data.currency,
|
currency: orderSummary.data.currency,
|
||||||
address: billingDetails,
|
address: billingDetails,
|
||||||
|
redirect_to: redirectTo.value,
|
||||||
|
payment_for_certificate: props.type == 'certificate',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const generatePaymentLink = () => {
|
const generatePaymentLink = () => {
|
||||||
|
console.log('called')
|
||||||
paymentLink.submit(
|
paymentLink.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
|
console.log('validation start')
|
||||||
if (!billingDetails.source) {
|
if (!billingDetails.source) {
|
||||||
return __('Please let us know where you heard about us from.')
|
return __('Please let us know where you heard about us from.')
|
||||||
}
|
}
|
||||||
@@ -330,6 +329,8 @@ const validateAddress = () => {
|
|||||||
!states.includes(billingDetails.state)
|
!states.includes(billingDetails.state)
|
||||||
)
|
)
|
||||||
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
|
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
|
||||||
|
|
||||||
|
console.log('validation address')
|
||||||
}
|
}
|
||||||
|
|
||||||
const showError = (err) => {
|
const showError = (err) => {
|
||||||
@@ -347,4 +348,14 @@ const changeCurrency = (country) => {
|
|||||||
billingDetails.country = country
|
billingDetails.country = country
|
||||||
orderSummary.reload()
|
orderSummary.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const redirectTo = computed(() => {
|
||||||
|
if (props.type == 'course') {
|
||||||
|
return `/lms/courses/${props.name}`
|
||||||
|
} else if (props.type == 'batch') {
|
||||||
|
return `/lms/batches/${props.name}`
|
||||||
|
} else if (props.type == 'certificate') {
|
||||||
|
return `/lms/courses/${props.name}/certification`
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
117
frontend/src/pages/CourseCertification.vue
Normal file
117
frontend/src/pages/CourseCertification.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
</header>
|
||||||
|
<div class="p-5">
|
||||||
|
<div v-if="certificate.data && Object.keys(certificate.data).length">
|
||||||
|
<div class="text-lg text-ink-gray-9 font-semibold mb-1">
|
||||||
|
{{ __('Certification') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-9 text-sm">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'You are already certified for this course. Click on the card below to open your certificate.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border p-3 w-fit min-w-60 rounded-md space-y-2 hover:bg-surface-gray-1 cursor-pointer mt-5"
|
||||||
|
@click="openCertificate(certificate.data)"
|
||||||
|
>
|
||||||
|
<div class="text-ink-gray-9 font-semibold">
|
||||||
|
{{ courseTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-ink-gray-7 font-medium">
|
||||||
|
{{ __('Issued On') }}:
|
||||||
|
{{ dayjs(certificate.data.issue_date).format('DD MMM YYYY') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<UpcomingEvaluations v-if="courses.length" :courses="courses" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
import { Breadcrumbs, call, createResource } from 'frappe-ui'
|
||||||
|
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||||
|
|
||||||
|
const courseTitle = ref(null)
|
||||||
|
const evaluator = ref(null)
|
||||||
|
const courses = ref([])
|
||||||
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCourseDetails()
|
||||||
|
})
|
||||||
|
|
||||||
|
const certificate = createResource({
|
||||||
|
url: 'frappe.client.get_value',
|
||||||
|
params: {
|
||||||
|
doctype: 'LMS Certificate',
|
||||||
|
filters: {
|
||||||
|
member: user.data?.name,
|
||||||
|
course: props.courseName,
|
||||||
|
},
|
||||||
|
fieldname: ['name', 'template', 'issue_date'],
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: [user.data?.name, props.courseName],
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCourseDetails = () => {
|
||||||
|
call('frappe.client.get_value', {
|
||||||
|
doctype: 'LMS Course',
|
||||||
|
filters: { name: props.courseName },
|
||||||
|
fieldname: ['title', 'evaluator'],
|
||||||
|
}).then((data) => {
|
||||||
|
courseTitle.value = data.title
|
||||||
|
evaluator.value = data.evaluator
|
||||||
|
populateCourses()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const populateCourses = () => {
|
||||||
|
courses.value = [
|
||||||
|
{
|
||||||
|
course: props.courseName,
|
||||||
|
title: courseTitle.value,
|
||||||
|
evaluator: evaluator.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCertificate = (certificate) => {
|
||||||
|
window.open(
|
||||||
|
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||||
|
certificate.name
|
||||||
|
}&format=${encodeURIComponent(certificate.template)}`,
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => [
|
||||||
|
{
|
||||||
|
label: __('Courses'),
|
||||||
|
route: { name: 'Courses' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: courseTitle.value,
|
||||||
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Certification'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5 mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-10 mb-4">
|
<div class="grid grid-cols-2 gap-10 mb-4">
|
||||||
<div
|
<div
|
||||||
v-if="user.data?.is_moderator"
|
v-if="user.data?.is_moderator"
|
||||||
class="flex flex-col space-y-4"
|
class="flex flex-col space-y-4"
|
||||||
@@ -188,43 +188,48 @@
|
|||||||
v-model="course.featured"
|
v-model="course.featured"
|
||||||
:label="__('Featured')"
|
:label="__('Featured')"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-3">
|
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.disable_self_learning"
|
v-model="course.disable_self_learning"
|
||||||
:label="__('Disable Self Enrollment')"
|
:label="__('Disable Self Enrollment')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
type="checkbox"
|
|
||||||
v-model="course.enable_certification"
|
|
||||||
:label="__('Completion Certificate')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container border-t">
|
<div class="container border-t space-y-4">
|
||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5">
|
||||||
{{ __('Pricing') }}
|
{{ __('Pricing and Certification') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="grid grid-cols-3">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.paid_course"
|
v-model="course.paid_course"
|
||||||
:label="__('Paid Course')"
|
:label="__('Paid Course')"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.enable_certification"
|
||||||
|
:label="__('Completion Certificate')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.paid_certificate"
|
||||||
|
:label="__('Paid Certificate')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl v-model="course.course_price" :label="__('Amount')" />
|
||||||
v-model="course.course_price"
|
|
||||||
:label="__('Course Price')"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
<Link
|
<Link
|
||||||
doctype="Currency"
|
doctype="Currency"
|
||||||
v-model="course.currency"
|
v-model="course.currency"
|
||||||
:filters="{ enabled: 1 }"
|
:filters="{ enabled: 1 }"
|
||||||
:label="__('Currency')"
|
:label="__('Currency')"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="course.paid_certificate"
|
||||||
|
doctype="Course Evaluator"
|
||||||
|
v-model="course.evaluator"
|
||||||
|
:label="__('Evaluator')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,8 +301,10 @@ const course = reactive({
|
|||||||
disable_self_learning: false,
|
disable_self_learning: false,
|
||||||
enable_certification: false,
|
enable_certification: false,
|
||||||
paid_course: false,
|
paid_course: false,
|
||||||
|
paid_certificate: false,
|
||||||
course_price: '',
|
course_price: '',
|
||||||
currency: '',
|
currency: '',
|
||||||
|
evaluator: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -391,6 +398,7 @@ const courseResource = createResource({
|
|||||||
'paid_course',
|
'paid_course',
|
||||||
'featured',
|
'featured',
|
||||||
'enable_certification',
|
'enable_certification',
|
||||||
|
'paid_certifiate',
|
||||||
]
|
]
|
||||||
for (let idx in checkboxes) {
|
for (let idx in checkboxes) {
|
||||||
let key = checkboxes[idx]
|
let key = checkboxes[idx]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
<CertificationLinks :courseName="courseName" />
|
||||||
</header>
|
</header>
|
||||||
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
||||||
<div
|
<div
|
||||||
@@ -197,13 +198,14 @@ 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 { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { ChevronLeft, ChevronRight, GraduationCap } 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'
|
||||||
import EditorJS from '@editorjs/editorjs'
|
import EditorJS from '@editorjs/editorjs'
|
||||||
import LessonContent from '@/components/LessonContent.vue'
|
import LessonContent from '@/components/LessonContent.vue'
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import ProgressBar from '@/components/ProgressBar.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const renderEditor = (holder) => {
|
|||||||
const lesson = reactive({
|
const lesson = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
include_in_preview: false,
|
include_in_preview: false,
|
||||||
body: 'Test',
|
body: '',
|
||||||
instructor_notes: '',
|
instructor_notes: '',
|
||||||
content: '',
|
content: '',
|
||||||
})
|
})
|
||||||
@@ -294,7 +294,7 @@ const convertToJSON = (lessonData) => {
|
|||||||
type: 'upload',
|
type: 'upload',
|
||||||
data: {
|
data: {
|
||||||
file_url: video,
|
file_url: video,
|
||||||
file_type: 'video',
|
file_type: video.split('.').pop(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (block.includes('{{ Audio')) {
|
} else if (block.includes('{{ Audio')) {
|
||||||
@@ -303,7 +303,7 @@ const convertToJSON = (lessonData) => {
|
|||||||
type: 'upload',
|
type: 'upload',
|
||||||
data: {
|
data: {
|
||||||
file_url: audio,
|
file_url: audio,
|
||||||
file_type: 'audio',
|
file_type: audio.split('.').pop(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (block.includes('{{ PDF')) {
|
} else if (block.includes('{{ PDF')) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
type="number"
|
type="number"
|
||||||
v-model="quiz.max_attempts"
|
v-model="quiz.max_attempts"
|
||||||
:label="__('Maximun Attempts')"
|
:label="__('Maximum Attempts')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ const routes = [
|
|||||||
component: () => import('@/pages/Lesson.vue'),
|
component: () => import('@/pages/Lesson.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/courses/:courseName/certification',
|
||||||
|
name: 'CourseCertification',
|
||||||
|
component: () => import('@/pages/CourseCertification.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/courses/:courseName/learn/:chapterName',
|
path: '/courses/:courseName/learn/:chapterName',
|
||||||
name: 'SCORMChapter',
|
name: 'SCORMChapter',
|
||||||
@@ -219,7 +225,7 @@ let router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
const { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
const { allowGuestAccess } = useSettings()
|
const { allowGuestAccess } = useSettings()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -50,10 +50,18 @@ export class Markdown {
|
|||||||
this.wrapper.innerHTML = this.text
|
this.wrapper.innerHTML = this.text
|
||||||
|
|
||||||
this.wrapper.addEventListener('keydown', (event) => {
|
this.wrapper.addEventListener('keydown', (event) => {
|
||||||
const value = event.target.textContent
|
let value = event.target.textContent
|
||||||
if (event.keyCode === 32 && value.startsWith('#')) {
|
if (event.keyCode === 32 && value.startsWith('#')) {
|
||||||
this.convertToHeader(event, value)
|
this.convertToHeader(event, value)
|
||||||
} else if (event.keyCode === 13) {
|
} else if (event.keyCode == 189) {
|
||||||
|
this.convertBlock('list', {
|
||||||
|
style: 'unordered',
|
||||||
|
})
|
||||||
|
} else if (/^[a-zA-Z]/.test(event.key)) {
|
||||||
|
this.convertBlock('paragraph', {
|
||||||
|
text: value,
|
||||||
|
})
|
||||||
|
} else if (event.keyCode === 13 || event.keyCode === 190) {
|
||||||
this.parseContent(event)
|
this.parseContent(event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -75,7 +83,11 @@ export class Markdown {
|
|||||||
|
|
||||||
parseContent(event) {
|
parseContent(event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const previousLine = this.wrapper.textContent
|
let previousLine = this.wrapper.textContent
|
||||||
|
if (event.keyCode === 190) {
|
||||||
|
previousLine = previousLine + '.'
|
||||||
|
}
|
||||||
|
|
||||||
if (previousLine && this.hasImage(previousLine)) {
|
if (previousLine && this.hasImage(previousLine)) {
|
||||||
this.wrapper.textContent = ''
|
this.wrapper.textContent = ''
|
||||||
this.convertBlock('image')
|
this.convertBlock('image')
|
||||||
@@ -94,12 +106,12 @@ export class Markdown {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
} else if (previousLine && previousLine.startsWith('1. ')) {
|
} else if (previousLine && previousLine.startsWith('1.')) {
|
||||||
this.convertBlock('list', {
|
this.convertBlock('list', {
|
||||||
style: 'ordered',
|
style: 'ordered',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
content: previousLine.replace('1. ', ''),
|
content: previousLine.replace('1.', ''),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -108,6 +120,10 @@ export class Markdown {
|
|||||||
this.convertBlock('embed', {
|
this.convertBlock('embed', {
|
||||||
source: previousLine,
|
source: previousLine,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this.convertBlock('paragraph', {
|
||||||
|
text: previousLine,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createApp, h } from 'vue'
|
|||||||
import { usersStore } from '../stores/user'
|
import { usersStore } from '../stores/user'
|
||||||
import translationPlugin from '../translation'
|
import translationPlugin from '../translation'
|
||||||
import { CircleHelp } from 'lucide-vue-next'
|
import { CircleHelp } from 'lucide-vue-next'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
export class Quiz {
|
export class Quiz {
|
||||||
constructor({ data, api, readOnly }) {
|
constructor({ data, api, readOnly }) {
|
||||||
@@ -46,6 +47,7 @@ export class Quiz {
|
|||||||
quiz: quiz,
|
quiz: quiz,
|
||||||
})
|
})
|
||||||
app.use(translationPlugin)
|
app.use(translationPlugin)
|
||||||
|
app.use(router)
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
app.provide('$user', userResource)
|
app.provide('$user', userResource)
|
||||||
app.mount(this.wrapper)
|
app.mount(this.wrapper)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['fs'],
|
allowedHosts: ['fs', 'bs'],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "2.22.0"
|
__version__ = "2.24.0"
|
||||||
|
|||||||
@@ -951,62 +951,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": null,
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payments_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "custom_css",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Payments",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.329032",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payments_tab",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
@@ -1119,230 +1063,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payment_gateway",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "accept_payment",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Payment Gateway",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.408659",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payment_gateway",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": "Payment Gateway",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "Buy Now",
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payment_button_label",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payment_gateway",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Button Label",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.439246",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payment_button_label",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": null,
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payments_cb",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payment_button_help",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": null,
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.491696",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payments_cb",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "eval:doc.accept_payment && !doc.amount_based_on_field",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "amount_field",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Amount",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.569591",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-amount",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
@@ -1399,174 +1119,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payment_button_help",
|
|
||||||
"fieldtype": "Text",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payment_button_label",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Button Help",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.466744",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payment_button_help",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount_based_on_field",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payments_cb",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Amount Based On Field",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.517344",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-amount_based_on_field",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "eval:doc.accept_payment && doc.amount_based_on_field",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount_field",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "amount_based_on_field",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Amount Field",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.543136",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-amount_field",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
@@ -1679,62 +1231,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "currency",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "amount",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Currency",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.595419",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-currency",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": "Currency",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ scheduler_events = {
|
|||||||
"daily": [
|
"daily": [
|
||||||
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
||||||
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
||||||
|
"lms.lms.doctype.lms_batch.lms_batch.send_batch_start_reminder",
|
||||||
|
"lms.lms.doctype.lms_live_class.lms_live_class.send_live_class_reminder",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
104
lms/lms/api.py
104
lms/lms/api.py
@@ -7,16 +7,12 @@ import zipfile
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import requests
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from frappe.translate import get_all_translations
|
from frappe.translate import get_all_translations
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import DocType
|
|
||||||
from frappe.query_builder.functions import Count
|
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
time_diff,
|
|
||||||
now_datetime,
|
|
||||||
get_datetime,
|
get_datetime,
|
||||||
|
getdate,
|
||||||
cint,
|
cint,
|
||||||
flt,
|
flt,
|
||||||
now,
|
now,
|
||||||
@@ -24,10 +20,10 @@ from frappe.utils import (
|
|||||||
format_date,
|
format_date,
|
||||||
date_diff,
|
date_diff,
|
||||||
)
|
)
|
||||||
from typing import Optional
|
|
||||||
from lms.lms.utils import get_average_rating, get_lesson_count
|
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||||
|
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -180,6 +176,7 @@ def get_user_info():
|
|||||||
user.is_moderator = "Moderator" in user.roles
|
user.is_moderator = "Moderator" in user.roles
|
||||||
user.is_evaluator = "Batch Evaluator" in user.roles
|
user.is_evaluator = "Batch Evaluator" in user.roles
|
||||||
user.is_student = "LMS Student" in user.roles
|
user.is_student = "LMS Student" in user.roles
|
||||||
|
user.is_fc_site = is_fc_site()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -193,24 +190,24 @@ def get_translations():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def validate_billing_access(type, name):
|
def validate_billing_access(billing_type, name):
|
||||||
access = True
|
access = True
|
||||||
message = ""
|
message = ""
|
||||||
doctype = "LMS Course" if type == "course" else "LMS Batch"
|
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
|
||||||
|
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
access = False
|
access = False
|
||||||
message = _("Please login to continue with payment.")
|
message = _("Please login to continue with payment.")
|
||||||
|
|
||||||
if type not in ["course", "batch"]:
|
if access and billing_type not in ["course", "batch", "certificate"]:
|
||||||
access = False
|
access = False
|
||||||
message = _("Module is incorrect.")
|
message = _("Module is incorrect.")
|
||||||
|
|
||||||
if not frappe.db.exists(doctype, name):
|
if access and not frappe.db.exists(doctype, name):
|
||||||
access = False
|
access = False
|
||||||
message = _("Module Name is incorrect or does not exist.")
|
message = _("Module Name is incorrect or does not exist.")
|
||||||
|
|
||||||
if type == "course":
|
if access and billing_type == "course":
|
||||||
membership = frappe.db.exists(
|
membership = frappe.db.exists(
|
||||||
"LMS Enrollment", {"member": frappe.session.user, "course": name}
|
"LMS Enrollment", {"member": frappe.session.user, "course": name}
|
||||||
)
|
)
|
||||||
@@ -218,7 +215,7 @@ def validate_billing_access(type, name):
|
|||||||
access = False
|
access = False
|
||||||
message = _("You are already enrolled for this course.")
|
message = _("You are already enrolled for this course.")
|
||||||
|
|
||||||
else:
|
elif access and billing_type == "batch":
|
||||||
membership = frappe.db.exists(
|
membership = frappe.db.exists(
|
||||||
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
|
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
|
||||||
)
|
)
|
||||||
@@ -226,6 +223,19 @@ def validate_billing_access(type, name):
|
|||||||
access = False
|
access = False
|
||||||
message = _("You are already enrolled for this batch.")
|
message = _("You are already enrolled for this batch.")
|
||||||
|
|
||||||
|
elif access and billing_type == "certificate":
|
||||||
|
purchased_certificate = frappe.db.exists(
|
||||||
|
"LMS Enrollment",
|
||||||
|
{
|
||||||
|
"course": name,
|
||||||
|
"member": frappe.session.user,
|
||||||
|
"purchased_certificate": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if purchased_certificate:
|
||||||
|
access = False
|
||||||
|
message = _("You have already purchased the certificate for this course.")
|
||||||
|
|
||||||
address = frappe.db.get_value(
|
address = frappe.db.get_value(
|
||||||
"Address",
|
"Address",
|
||||||
{"email_id": frappe.session.user},
|
{"email_id": frappe.session.user},
|
||||||
@@ -373,7 +383,7 @@ def get_evaluator_details(evaluator):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_certified_participants(filters=None, start=0, page_length=30, search=None):
|
def get_certified_participants(filters=None, start=0, page_length=30):
|
||||||
or_filters = {}
|
or_filters = {}
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -845,7 +855,7 @@ def update_course_statistics():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_announcements(batch):
|
def get_announcements(batch):
|
||||||
return frappe.get_all(
|
communications = frappe.get_all(
|
||||||
"Communication",
|
"Communication",
|
||||||
filters={
|
filters={
|
||||||
"reference_doctype": "LMS Batch",
|
"reference_doctype": "LMS Batch",
|
||||||
@@ -863,6 +873,13 @@ def get_announcements(batch):
|
|||||||
order_by="communication_date desc",
|
order_by="communication_date desc",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for communication in communications:
|
||||||
|
communication.image = frappe.get_cached_value(
|
||||||
|
"User", communication.sender, "user_image"
|
||||||
|
)
|
||||||
|
|
||||||
|
return communications
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def delete_course(course):
|
def delete_course(course):
|
||||||
@@ -1228,13 +1245,54 @@ def is_guest_allowed():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_announcement(students, cc, subject, content):
|
def cancel_evaluation(evaluation):
|
||||||
for student in students:
|
evaluation = frappe._dict(evaluation)
|
||||||
frappe.sendmail(
|
|
||||||
recipients=student,
|
if evaluation.member != frappe.session.user:
|
||||||
cc=cc,
|
return
|
||||||
subject=subject,
|
|
||||||
message=content,
|
frappe.db.set_value("LMS Certificate Request", evaluation.name, "status", "Cancelled")
|
||||||
header=[subject, "green"],
|
events = frappe.get_all(
|
||||||
retry=3,
|
"Event Participants",
|
||||||
|
{
|
||||||
|
"email": evaluation.member,
|
||||||
|
},
|
||||||
|
["parent", "name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
info = frappe.db.get_value("Event", event.parent, ["starts_on", "subject"], as_dict=1)
|
||||||
|
date = str(info.starts_on).split(" ")[0]
|
||||||
|
|
||||||
|
if (
|
||||||
|
date == str(evaluation.date.format("YYYY-MM-DD"))
|
||||||
|
and evaluation.member_name in info.subject
|
||||||
|
):
|
||||||
|
communication = frappe.db.get_value(
|
||||||
|
"Communication",
|
||||||
|
{"reference_doctype": "Event", "reference_name": event.parent},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
if communication:
|
||||||
|
frappe.delete_doc("Communication", communication, ignore_permissions=True)
|
||||||
|
|
||||||
|
frappe.delete_doc("Event Participants", event.name, ignore_permissions=True)
|
||||||
|
frappe.delete_doc("Event", event.parent, ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_certification_details(course):
|
||||||
|
membership = None
|
||||||
|
filters = {"course": course, "member": frappe.session.user}
|
||||||
|
|
||||||
|
if frappe.db.exists("LMS Enrollment", filters):
|
||||||
|
membership = frappe.db.get_value(
|
||||||
|
"LMS Enrollment",
|
||||||
|
filters,
|
||||||
|
["name", "certificate", "purchased_certificate"],
|
||||||
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
paid_certificate = frappe.db.get_value("LMS Course", course, "paid_certificate")
|
||||||
|
|
||||||
|
return {"membership": membership, "paid_certificate": paid_certificate}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-15 18:45:08.614466",
|
"modified": "2025-02-24 12:17:08.436659",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Evaluator",
|
"name": "Course Evaluator",
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"evaluator",
|
||||||
"section_break_dlzh",
|
"section_break_dlzh",
|
||||||
"assignment_attachment",
|
"assignment_attachment",
|
||||||
"answer",
|
"answer",
|
||||||
|
"section_break_ydgh",
|
||||||
"column_break_oqqy",
|
"column_break_oqqy",
|
||||||
"evaluator",
|
|
||||||
"status",
|
"status",
|
||||||
"comments",
|
"comments",
|
||||||
"section_break_rqal",
|
"section_break_rqal",
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "comments",
|
"fieldname": "comments",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Text Editor",
|
||||||
"label": "Comments"
|
"label": "Comments"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,12 +140,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_oqqy",
|
"fieldname": "column_break_oqqy",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ydgh",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-12-24 21:22:35.212732",
|
"modified": "2025-02-17 18:40:53.374932",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Assignment Submission",
|
"name": "LMS Assignment Submission",
|
||||||
|
|||||||
@@ -8,25 +8,31 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"published",
|
"section_break_earo",
|
||||||
"title",
|
"title",
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"allow_self_enrollment",
|
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"timezone",
|
"timezone",
|
||||||
"section_break_rgfj",
|
"section_break_cssv",
|
||||||
"medium",
|
"published",
|
||||||
"category",
|
"column_break_wfkz",
|
||||||
"column_break_flwy",
|
"allow_self_enrollment",
|
||||||
"seat_count",
|
"column_break_vnrp",
|
||||||
"evaluation_end_date",
|
"certification",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"description",
|
"description",
|
||||||
"column_break_hlqw",
|
"column_break_hlqw",
|
||||||
"instructors",
|
"instructors",
|
||||||
|
"section_break_rgfj",
|
||||||
|
"medium",
|
||||||
|
"category",
|
||||||
|
"confirmation_email_template",
|
||||||
|
"column_break_flwy",
|
||||||
|
"seat_count",
|
||||||
|
"evaluation_end_date",
|
||||||
"meta_image",
|
"meta_image",
|
||||||
"section_break_khcn",
|
"section_break_khcn",
|
||||||
"batch_details",
|
"batch_details",
|
||||||
@@ -206,6 +212,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "published",
|
"fieldname": "published",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Published"
|
"label": "Published"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,6 +325,35 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_khcn",
|
"fieldname": "section_break_khcn",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "confirmation_email_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Confirmation Email Template",
|
||||||
|
"options": "Email Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wfkz",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vnrp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "certification",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Certification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_earo",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_cssv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -335,7 +371,7 @@
|
|||||||
"link_fieldname": "batch_name"
|
"link_fieldname": "batch_name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-02-12 11:59:35.312487",
|
"modified": "2025-02-18 15:43:18.512504",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import json
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, format_datetime, get_time
|
from frappe.utils import cint, format_datetime, get_time, add_days, nowdate
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
get_lessons,
|
|
||||||
get_lesson_index,
|
get_lesson_index,
|
||||||
get_lesson_url,
|
get_lesson_url,
|
||||||
get_quiz_details,
|
get_quiz_details,
|
||||||
@@ -176,6 +175,10 @@ def create_live_class(
|
|||||||
class_details = frappe.get_doc(payload)
|
class_details = frappe.get_doc(payload)
|
||||||
class_details.save()
|
class_details.save()
|
||||||
return class_details
|
return class_details
|
||||||
|
else:
|
||||||
|
frappe.throw(
|
||||||
|
_("Error creating live class. Please try again. {0}").format(response.text)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
def authenticate():
|
||||||
@@ -254,17 +257,6 @@ def create_batch(
|
|||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def fetch_lessons(courses):
|
|
||||||
lessons = []
|
|
||||||
courses = json.loads(courses)
|
|
||||||
|
|
||||||
for course in courses:
|
|
||||||
lessons.extend(get_lessons(course.get("course")))
|
|
||||||
|
|
||||||
return lessons
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_course(course, parent, name=None, evaluator=None):
|
def add_course(course, parent, name=None, evaluator=None):
|
||||||
frappe.only_for("Moderator")
|
frappe.only_for("Moderator")
|
||||||
@@ -405,3 +397,40 @@ def is_milestone_complete(idx, batch):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def send_batch_start_reminder():
|
||||||
|
batches = frappe.get_all(
|
||||||
|
"LMS Batch",
|
||||||
|
{"start_date": add_days(nowdate(), 1), "published": 1},
|
||||||
|
["name", "title", "start_date", "start_time", "medium"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
students = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment", {"batch": batch}, ["member", "member_name"]
|
||||||
|
)
|
||||||
|
for student in students:
|
||||||
|
send_mail(batch, student)
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(batch, student):
|
||||||
|
subject = _("Batch Start Reminder")
|
||||||
|
template = "batch_start_reminder"
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"student_name": student.member_name,
|
||||||
|
"title": batch.title,
|
||||||
|
"start_date": batch.start_date,
|
||||||
|
"start_time": batch.start_time,
|
||||||
|
"medium": batch.medium,
|
||||||
|
"name": batch.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=student.member,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
header=[_(f"Batch Start Reminder: {batch.title}"), "orange"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -76,17 +76,26 @@ def send_confirmation_email(doc):
|
|||||||
|
|
||||||
|
|
||||||
def send_mail(doc):
|
def send_mail(doc):
|
||||||
subject = _("Enrollment Confirmation for the Next Training Batch")
|
|
||||||
template = "batch_confirmation"
|
|
||||||
custom_template = frappe.db.get_single_value(
|
|
||||||
"LMS Settings", "batch_confirmation_template"
|
|
||||||
)
|
|
||||||
batch = frappe.db.get_value(
|
batch = frappe.db.get_value(
|
||||||
"LMS Batch",
|
"LMS Batch",
|
||||||
doc.batch,
|
doc.batch,
|
||||||
["name", "title", "start_date", "start_time", "medium"],
|
[
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"start_date",
|
||||||
|
"start_time",
|
||||||
|
"medium",
|
||||||
|
"confirmation_email_template",
|
||||||
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
subject = _("Enrollment Confirmation for {0}").format(batch.title)
|
||||||
|
template = "batch_confirmation"
|
||||||
|
custom_template = batch.confirmation_email_template or frappe.db.get_single_value(
|
||||||
|
"LMS Settings", "batch_confirmation_template"
|
||||||
|
)
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"title": batch.title,
|
"title": batch.title,
|
||||||
"student_name": doc.member_name,
|
"student_name": doc.member_name,
|
||||||
@@ -107,6 +116,6 @@ def send_mail(doc):
|
|||||||
template=template if not custom_template else None,
|
template=template if not custom_template else None,
|
||||||
content=content if custom_template else None,
|
content=content if custom_template else None,
|
||||||
args=args,
|
args=args,
|
||||||
header=[subject, "green"],
|
header=[_(batch.title), "green"],
|
||||||
retry=3,
|
retry=3,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-17 11:57:02.859109",
|
"modified": "2025-02-19 16:26:05.902473",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
@@ -156,6 +156,7 @@
|
|||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
|
|||||||
@@ -122,11 +122,6 @@ def create_certificate(course):
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
expires_after_yrs = int(frappe.db.get_value("LMS Course", course, "expiry"))
|
|
||||||
expiry_date = None
|
|
||||||
if expires_after_yrs:
|
|
||||||
expiry_date = add_years(nowdate(), expires_after_yrs)
|
|
||||||
|
|
||||||
default_certificate_template = frappe.db.get_value(
|
default_certificate_template = frappe.db.get_value(
|
||||||
"Property Setter",
|
"Property Setter",
|
||||||
{
|
{
|
||||||
@@ -148,7 +143,6 @@ def create_certificate(course):
|
|||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
"course": course,
|
"course": course,
|
||||||
"issue_date": nowdate(),
|
"issue_date": nowdate(),
|
||||||
"expiry_date": expiry_date,
|
|
||||||
"template": default_certificate_template,
|
"template": default_certificate_template,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class TestLMSCertificate(unittest.TestCase):
|
|||||||
"Test Certificate",
|
"Test Certificate",
|
||||||
{
|
{
|
||||||
"enable_certification": 1,
|
"enable_certification": 1,
|
||||||
"expiry": 2,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
certificate = create_certificate(course.name)
|
certificate = create_certificate(course.name)
|
||||||
@@ -24,7 +23,6 @@ class TestLMSCertificate(unittest.TestCase):
|
|||||||
self.assertEqual(certificate.member, "Administrator")
|
self.assertEqual(certificate.member, "Administrator")
|
||||||
self.assertEqual(certificate.course, course.name)
|
self.assertEqual(certificate.course, course.name)
|
||||||
self.assertEqual(certificate.issue_date, nowdate())
|
self.assertEqual(certificate.issue_date, nowdate())
|
||||||
self.assertEqual(certificate.expiry_date, add_years(nowdate(), cint(course.expiry)))
|
|
||||||
|
|
||||||
frappe.db.delete("LMS Certificate", certificate.name)
|
frappe.db.delete("LMS Certificate", certificate.name)
|
||||||
frappe.db.delete("LMS Course", course.name)
|
frappe.db.delete("LMS Course", course.name)
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"google_meet_link",
|
"google_meet_link",
|
||||||
"column_break_ddyh",
|
"column_break_ddyh",
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time"
|
"end_time",
|
||||||
|
"status"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -144,11 +145,19 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Batch Title"
|
"label": "Batch Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Upcoming\nCompleted\nCancelled",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-09-11 11:19:44.669132",
|
"modified": "2025-02-19 17:20:02.526294",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
@@ -204,6 +213,19 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [
|
||||||
|
{
|
||||||
|
"color": "Blue",
|
||||||
|
"title": "Upcoming"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Green",
|
||||||
|
"title": "Completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Red",
|
||||||
|
"title": "Cancelled"
|
||||||
|
}
|
||||||
|
],
|
||||||
"title_field": "member_name"
|
"title_field": "member_name"
|
||||||
}
|
}
|
||||||
@@ -40,15 +40,13 @@
|
|||||||
"pricing_tab",
|
"pricing_tab",
|
||||||
"pricing_section",
|
"pricing_section",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
|
"enable_certification",
|
||||||
|
"paid_certificate",
|
||||||
|
"evaluator",
|
||||||
"column_break_acoj",
|
"column_break_acoj",
|
||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
"certification_tab",
|
|
||||||
"certification_section",
|
|
||||||
"enable_certification",
|
|
||||||
"column_break_rxww",
|
|
||||||
"expiry",
|
|
||||||
"tab_4_tab",
|
"tab_4_tab",
|
||||||
"statistics_section",
|
"statistics_section",
|
||||||
"enrollments",
|
"enrollments",
|
||||||
@@ -134,22 +132,11 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Course Settings"
|
"label": "Course Settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "certification_section",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "enable_certification",
|
"fieldname": "enable_certification",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Certification"
|
"label": "Completion Certificate"
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "enable_certification",
|
|
||||||
"fieldname": "expiry",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Certification Expires After (Years)"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "related_courses",
|
"fieldname": "related_courses",
|
||||||
@@ -181,7 +168,6 @@
|
|||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
@@ -195,22 +181,16 @@
|
|||||||
"label": "Paid Course"
|
"label": "Paid Course"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
|
||||||
"fieldname": "course_price",
|
"fieldname": "course_price",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Course Price",
|
"label": "Amount",
|
||||||
"mandatory_depends_on": "paid_course"
|
"mandatory_depends_on": "paid_course"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_rxww",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_acoj",
|
"fieldname": "column_break_acoj",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
|
||||||
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
||||||
"fieldname": "amount_usd",
|
"fieldname": "amount_usd",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
@@ -238,12 +218,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "pricing_tab",
|
"fieldname": "pricing_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Pricing"
|
"label": "Pricing and Certification"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "certification_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Certification"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_htgn",
|
"fieldname": "column_break_htgn",
|
||||||
@@ -284,6 +259,19 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Rating",
|
"label": "Rating",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "paid_certificate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Paid Certificate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "paid_certificate",
|
||||||
|
"fieldname": "evaluator",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Evaluator",
|
||||||
|
"options": "Course Evaluator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -310,7 +298,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-10-30 23:08:31.842860",
|
"modified": "2025-02-24 11:50:58.325804",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import json
|
|||||||
import random
|
import random
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, today
|
from frappe.utils import today, cint
|
||||||
from frappe.utils.telemetry import capture
|
from lms.lms.utils import get_chapters
|
||||||
from lms.lms.utils import get_chapters, can_create_courses
|
|
||||||
from ...utils import generate_slug, validate_image, update_payment_record
|
from ...utils import generate_slug, validate_image, update_payment_record
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ class LMSCourse(Document):
|
|||||||
self.validate_video_link()
|
self.validate_video_link()
|
||||||
self.validate_status()
|
self.validate_status()
|
||||||
self.validate_payments_app()
|
self.validate_payments_app()
|
||||||
|
self.validate_certification()
|
||||||
self.validate_amount_and_currency()
|
self.validate_amount_and_currency()
|
||||||
self.image = validate_image(self.image)
|
self.image = validate_image(self.image)
|
||||||
|
|
||||||
@@ -52,10 +52,22 @@ class LMSCourse(Document):
|
|||||||
if "payments" not in installed_apps:
|
if "payments" not in installed_apps:
|
||||||
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
||||||
|
|
||||||
|
def validate_certification(self):
|
||||||
|
if self.enable_certification and self.paid_certificate:
|
||||||
|
frappe.throw(
|
||||||
|
_("A course cannot have both paid certificate and certificate of completion.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.paid_certificate and not self.evaluator:
|
||||||
|
frappe.throw(_("Evaluator is required for paid certificates."))
|
||||||
|
|
||||||
def validate_amount_and_currency(self):
|
def validate_amount_and_currency(self):
|
||||||
if self.paid_course and (not self.course_price and not self.currency):
|
if self.paid_course and (cint(self.course_price) < 0 or not self.currency):
|
||||||
frappe.throw(_("Amount and currency are required for paid courses."))
|
frappe.throw(_("Amount and currency are required for paid courses."))
|
||||||
|
|
||||||
|
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
|
||||||
|
frappe.throw(_("Amount and currency are required for paid certificates."))
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||||
self.send_email_to_interested_users()
|
self.send_email_to_interested_users()
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"member_username",
|
"member_username",
|
||||||
|
"certification_section",
|
||||||
|
"purchased_certificate",
|
||||||
|
"certificate",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"cohort",
|
"cohort",
|
||||||
"subgroup",
|
"subgroup",
|
||||||
@@ -123,11 +126,28 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Payment",
|
"label": "Payment",
|
||||||
"options": "LMS Payment"
|
"options": "LMS Payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certification_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Certification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "purchased_certificate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Purchased Certificate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certificate",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Certificate",
|
||||||
|
"options": "LMS Certificate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-10-30 12:44:16.103598",
|
"modified": "2025-02-21 17:11:37.986157",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Enrollment",
|
"name": "LMS Enrollment",
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.utils import cint, get_datetime
|
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
||||||
|
|
||||||
|
|
||||||
class LMSLiveClass(Document):
|
class LMSLiveClass(Document):
|
||||||
@@ -56,8 +57,48 @@ class LMSLiveClass(Document):
|
|||||||
{
|
{
|
||||||
"sync_with_google_calendar": 1,
|
"sync_with_google_calendar": 1,
|
||||||
"google_calendar": calendar,
|
"google_calendar": calendar,
|
||||||
"description": f"A Live Class has been scheduled on {frappe.utils.format_date(self.date, 'medium')} at { frappe.utils.format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
|
"description": f"A Live Class has been scheduled on {format_date(self.date, 'medium')} at {format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
|
def send_live_class_reminder():
|
||||||
|
classes = frappe.get_all(
|
||||||
|
"LMS Live Class",
|
||||||
|
{
|
||||||
|
"date": nowdate(),
|
||||||
|
},
|
||||||
|
["name", "batch_name", "title", "date", "time"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for live_class in classes:
|
||||||
|
students = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment",
|
||||||
|
{"batch": live_class.batch_name},
|
||||||
|
["member", "member_name"],
|
||||||
|
)
|
||||||
|
for student in students:
|
||||||
|
send_mail(live_class, student)
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(live_class, student):
|
||||||
|
subject = f"Your class on {live_class.title} is tomorrow"
|
||||||
|
template = "live_class_reminder"
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"student_name": student.member_name,
|
||||||
|
"title": live_class.title,
|
||||||
|
"date": live_class.date,
|
||||||
|
"time": live_class.time,
|
||||||
|
"batch_name": live_class.batch_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=student.member,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"payment_for_document_type",
|
"payment_for_document_type",
|
||||||
"payment_for_document",
|
"payment_for_document",
|
||||||
"payment_received",
|
"payment_received",
|
||||||
|
"payment_for_certificate",
|
||||||
"payment_details_section",
|
"payment_details_section",
|
||||||
"currency",
|
"currency",
|
||||||
"amount",
|
"amount",
|
||||||
@@ -136,11 +137,26 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Source",
|
"label": "Source",
|
||||||
"options": "LMS Source"
|
"options": "LMS Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "payment_for_certificate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Payment for Certificate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2025-02-11 14:48:27.801895",
|
{
|
||||||
|
"link_doctype": "LMS Batch Enrollment",
|
||||||
|
"link_fieldname": "payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link_doctype": "LMS Enrollment",
|
||||||
|
"link_fieldname": "payment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2025-02-21 18:29:55.436611",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Payment",
|
"name": "LMS Payment",
|
||||||
|
|||||||
@@ -10,12 +10,29 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
|
|||||||
|
|
||||||
class LMSQuizSubmission(Document):
|
class LMSQuizSubmission(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_if_max_attempts_exceeded()
|
||||||
self.validate_marks()
|
self.validate_marks()
|
||||||
self.set_percentage()
|
self.set_percentage()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.notify_member()
|
self.notify_member()
|
||||||
|
|
||||||
|
def validate_if_max_attempts_exceeded(self):
|
||||||
|
max_attempts = frappe.db.get_value("LMS Quiz", self.quiz, ["max_attempts"])
|
||||||
|
if max_attempts == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_user_submission_count = frappe.db.count(
|
||||||
|
self.doctype, filters={"quiz": self.quiz, "member": frappe.session.user}
|
||||||
|
)
|
||||||
|
if current_user_submission_count >= max_attempts:
|
||||||
|
frappe.throw(
|
||||||
|
_("You have exceeded the maximum number of attempts ({0}) for this quiz").format(
|
||||||
|
max_attempts
|
||||||
|
),
|
||||||
|
MaximumAttemptsExceededError,
|
||||||
|
)
|
||||||
|
|
||||||
def validate_marks(self):
|
def validate_marks(self):
|
||||||
self.score = 0
|
self.score = 0
|
||||||
for row in self.result:
|
for row in self.result:
|
||||||
@@ -52,3 +69,7 @@ class LMSQuizSubmission(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
make_notification_logs(notification, [self.member])
|
make_notification_logs(notification, [self.member])
|
||||||
|
|
||||||
|
|
||||||
|
class MaximumAttemptsExceededError(frappe.DuplicateEntryError):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -18,19 +18,26 @@ def validate_currency(payment_gateway, currency):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_link(doctype, docname, title, amount, total_amount, currency, address):
|
def get_payment_link(
|
||||||
|
doctype,
|
||||||
|
docname,
|
||||||
|
title,
|
||||||
|
amount,
|
||||||
|
total_amount,
|
||||||
|
currency,
|
||||||
|
address,
|
||||||
|
redirect_to,
|
||||||
|
payment_for_certificate,
|
||||||
|
):
|
||||||
payment_gateway = get_payment_gateway()
|
payment_gateway = get_payment_gateway()
|
||||||
address = frappe._dict(address)
|
address = frappe._dict(address)
|
||||||
amount_with_gst = total_amount if total_amount != amount else 0
|
amount_with_gst = total_amount if total_amount != amount else 0
|
||||||
|
|
||||||
payment = record_payment(address, doctype, docname, amount, currency, amount_with_gst)
|
payment = record_payment(
|
||||||
|
address, doctype, docname, amount, currency, amount_with_gst, payment_for_certificate
|
||||||
|
)
|
||||||
controller = get_controller(payment_gateway)
|
controller = get_controller(payment_gateway)
|
||||||
|
|
||||||
if doctype == "LMS Course":
|
|
||||||
redirect_to = f"/lms/courses/{docname}/learn/1-1"
|
|
||||||
elif doctype == "LMS Batch":
|
|
||||||
redirect_to = f"/lms/batches/{docname}"
|
|
||||||
|
|
||||||
payment_details = {
|
payment_details = {
|
||||||
"amount": total_amount,
|
"amount": total_amount,
|
||||||
"title": f"Payment for {doctype} {title} {docname}",
|
"title": f"Payment for {doctype} {title} {docname}",
|
||||||
@@ -53,7 +60,15 @@ def get_payment_link(doctype, docname, title, amount, total_amount, currency, ad
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def record_payment(address, doctype, docname, amount, currency, amount_with_gst=0):
|
def record_payment(
|
||||||
|
address,
|
||||||
|
doctype,
|
||||||
|
docname,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
amount_with_gst=0,
|
||||||
|
payment_for_certificate=0,
|
||||||
|
):
|
||||||
address = frappe._dict(address)
|
address = frappe._dict(address)
|
||||||
address_name = save_address(address)
|
address_name = save_address(address)
|
||||||
|
|
||||||
@@ -71,6 +86,7 @@ def record_payment(address, doctype, docname, amount, currency, amount_with_gst=
|
|||||||
"source": address.source,
|
"source": address.source,
|
||||||
"payment_for_document_type": doctype,
|
"payment_for_document_type": doctype,
|
||||||
"payment_for_document": docname,
|
"payment_for_document": docname,
|
||||||
|
"payment_for_certificate": payment_for_certificate,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
payment_doc.save(ignore_permissions=True)
|
payment_doc.save(ignore_permissions=True)
|
||||||
|
|||||||
144
lms/lms/utils.py
144
lms/lms/utils.py
@@ -68,27 +68,26 @@ def generate_slug(title, doctype):
|
|||||||
return slugify(title, used_slugs=slugs)
|
return slugify(title, used_slugs=slugs)
|
||||||
|
|
||||||
|
|
||||||
def get_membership(course, member=None, batch=None):
|
def get_membership(course, member=None):
|
||||||
if not member:
|
if not member:
|
||||||
member = frappe.session.user
|
member = frappe.session.user
|
||||||
|
|
||||||
filters = {"member": member, "course": course}
|
filters = {"member": member, "course": course}
|
||||||
if batch:
|
|
||||||
filters["batch_old"] = batch
|
|
||||||
|
|
||||||
is_member = frappe.db.exists("LMS Enrollment", filters)
|
if frappe.db.exists("LMS Enrollment", filters):
|
||||||
if is_member:
|
|
||||||
membership = frappe.db.get_value(
|
membership = frappe.db.get_value(
|
||||||
"LMS Enrollment",
|
"LMS Enrollment",
|
||||||
filters,
|
filters,
|
||||||
["name", "batch_old", "current_lesson", "member_type", "progress", "member"],
|
[
|
||||||
|
"name",
|
||||||
|
"current_lesson",
|
||||||
|
"progress",
|
||||||
|
"member",
|
||||||
|
"purchased_certificate",
|
||||||
|
"certificate",
|
||||||
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if membership and membership.batch_old:
|
|
||||||
membership.batch_title = frappe.db.get_value(
|
|
||||||
"LMS Batch Old", membership.batch_old, "title"
|
|
||||||
)
|
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -856,26 +855,44 @@ def is_onboarding_complete():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_evaluator(course, batch):
|
def get_evaluator(course, batch=None):
|
||||||
evaluator = None
|
evaluator = None
|
||||||
evaluator = frappe.db.get_value(
|
if batch:
|
||||||
"Batch Course",
|
evaluator = frappe.db.get_value(
|
||||||
{"parent": batch, "course": course},
|
"Batch Course",
|
||||||
"evaluator",
|
{"parent": batch, "course": course},
|
||||||
)
|
"evaluator",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
||||||
return evaluator
|
return evaluator
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_upcoming_evals(student, courses):
|
def get_upcoming_evals(student, courses, batch=None):
|
||||||
|
filters = {
|
||||||
|
"member": student,
|
||||||
|
"course": ["in", courses],
|
||||||
|
"date": [">=", frappe.utils.nowdate()],
|
||||||
|
"status": "Upcoming",
|
||||||
|
}
|
||||||
|
|
||||||
|
if batch:
|
||||||
|
filters["batch_name"] = batch
|
||||||
|
|
||||||
upcoming_evals = frappe.get_all(
|
upcoming_evals = frappe.get_all(
|
||||||
"LMS Certificate Request",
|
"LMS Certificate Request",
|
||||||
{
|
filters,
|
||||||
"member": student,
|
[
|
||||||
"course": ["in", courses],
|
"name",
|
||||||
"date": [">=", frappe.utils.nowdate()],
|
"date",
|
||||||
},
|
"start_time",
|
||||||
["date", "start_time", "course", "evaluator", "google_meet_link"],
|
"course",
|
||||||
|
"evaluator",
|
||||||
|
"google_meet_link",
|
||||||
|
"member",
|
||||||
|
"member_name",
|
||||||
|
],
|
||||||
order_by="date",
|
order_by="date",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -999,6 +1016,7 @@ def get_course_details(course):
|
|||||||
"category",
|
"category",
|
||||||
"status",
|
"status",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
|
"paid_certificate",
|
||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
@@ -1013,7 +1031,7 @@ def get_course_details(course):
|
|||||||
|
|
||||||
course_details.instructors = get_instructors(course_details.name)
|
course_details.instructors = get_instructors(course_details.name)
|
||||||
# course_details.is_instructor = is_instructor(course_details.name)
|
# course_details.is_instructor = is_instructor(course_details.name)
|
||||||
if course_details.paid_course:
|
if course_details.paid_course or course_details.paid_certificate:
|
||||||
"""course_details.course_price, course_details.currency = check_multicurrency(
|
"""course_details.course_price, course_details.currency = check_multicurrency(
|
||||||
course_details.course_price, course_details.currency, None, course_details.amount_usd
|
course_details.course_price, course_details.currency, None, course_details.amount_usd
|
||||||
)"""
|
)"""
|
||||||
@@ -1126,14 +1144,21 @@ def get_lesson(course, chapter, lesson):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
membership = get_membership(course)
|
membership = get_membership(course)
|
||||||
course_title = frappe.db.get_value("LMS Course", course, "title")
|
course_info = frappe.db.get_value(
|
||||||
|
"LMS Course", course, ["title", "paid_certificate"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not lesson_details.include_in_preview
|
not lesson_details.include_in_preview
|
||||||
and not membership
|
and not membership
|
||||||
and not has_course_moderator_role()
|
and not has_course_moderator_role()
|
||||||
and not is_instructor(course)
|
and not is_instructor(course)
|
||||||
):
|
):
|
||||||
return {"no_preview": 1, "title": lesson_details.title, "course_title": course_title}
|
return {
|
||||||
|
"no_preview": 1,
|
||||||
|
"title": lesson_details.title,
|
||||||
|
"course_title": course_info.title,
|
||||||
|
}
|
||||||
|
|
||||||
lesson_details = frappe.db.get_value(
|
lesson_details = frappe.db.get_value(
|
||||||
"Course Lesson",
|
"Course Lesson",
|
||||||
@@ -1168,7 +1193,8 @@ def get_lesson(course, chapter, lesson):
|
|||||||
lesson_details.prev = neighbours["prev"]
|
lesson_details.prev = neighbours["prev"]
|
||||||
lesson_details.membership = membership
|
lesson_details.membership = membership
|
||||||
lesson_details.instructors = get_instructors(course)
|
lesson_details.instructors = get_instructors(course)
|
||||||
lesson_details.course_title = course_title
|
lesson_details.course_title = course_info.title
|
||||||
|
lesson_details.paid_certificate = course_info.paid_certificate
|
||||||
return lesson_details
|
return lesson_details
|
||||||
|
|
||||||
|
|
||||||
@@ -1194,6 +1220,16 @@ def get_neighbour_lesson(course, chapter, lesson):
|
|||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_batch_details(batch):
|
def get_batch_details(batch):
|
||||||
|
batch_students = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not frappe.db.get_value("LMS Batch", batch, "published")
|
||||||
|
and has_student_role()
|
||||||
|
and frappe.session.user not in batch_students
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
batch_details = frappe.db.get_value(
|
batch_details = frappe.db.get_value(
|
||||||
"LMS Batch",
|
"LMS Batch",
|
||||||
batch,
|
batch,
|
||||||
@@ -1215,6 +1251,7 @@ def get_batch_details(batch):
|
|||||||
"paid_batch",
|
"paid_batch",
|
||||||
"evaluation_end_date",
|
"evaluation_end_date",
|
||||||
"allow_self_enrollment",
|
"allow_self_enrollment",
|
||||||
|
"certification",
|
||||||
"timezone",
|
"timezone",
|
||||||
"category",
|
"category",
|
||||||
],
|
],
|
||||||
@@ -1222,13 +1259,12 @@ def get_batch_details(batch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
batch_details.instructors = get_instructors(batch)
|
batch_details.instructors = get_instructors(batch)
|
||||||
|
batch_details.accept_enrollments = batch_details.start_date > getdate()
|
||||||
|
|
||||||
batch_details.courses = frappe.get_all(
|
batch_details.courses = frappe.get_all(
|
||||||
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
|
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
|
||||||
)
|
)
|
||||||
batch_details.students = frappe.get_all(
|
batch_details.students = batch_students
|
||||||
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
|
|
||||||
)
|
|
||||||
|
|
||||||
if batch_details.paid_batch and batch_details.start_date >= getdate():
|
if batch_details.paid_batch and batch_details.start_date >= getdate():
|
||||||
batch_details.amount, batch_details.currency = check_multicurrency(
|
batch_details.amount, batch_details.currency = check_multicurrency(
|
||||||
@@ -1450,7 +1486,7 @@ def get_batch_students(batch):
|
|||||||
)
|
)
|
||||||
detail.assessments[title] = assessment_info
|
detail.assessments[title] = assessment_info
|
||||||
|
|
||||||
if assessment_info.result == "Passed":
|
if assessment_info.result == "Pass":
|
||||||
assessments_completed += 1
|
assessments_completed += 1
|
||||||
|
|
||||||
detail.courses_completed = courses_completed
|
detail.courses_completed = courses_completed
|
||||||
@@ -1493,20 +1529,26 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
|
|||||||
|
|
||||||
attempt = frappe.db.exists(doctype, filters)
|
attempt = frappe.db.exists(doctype, filters)
|
||||||
if attempt:
|
if attempt:
|
||||||
attempt_details = frappe.db.get_value(doctype, filters, fields)
|
fields.append("name")
|
||||||
|
attempt_details = frappe.db.get_value(doctype, filters, fields, as_dict=1)
|
||||||
if assessment_type == "LMS Quiz":
|
if assessment_type == "LMS Quiz":
|
||||||
result = "Failed"
|
result = "Failed"
|
||||||
passing_percentage = frappe.db.get_value(
|
passing_percentage = frappe.db.get_value(
|
||||||
"LMS Quiz", assessment, "passing_percentage"
|
"LMS Quiz", assessment, "passing_percentage"
|
||||||
)
|
)
|
||||||
if attempt_details >= passing_percentage:
|
if attempt_details.percentage >= passing_percentage:
|
||||||
result = "Passed"
|
result = "Pass"
|
||||||
else:
|
else:
|
||||||
result = attempt_details
|
result = attempt_details.status
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
{
|
{
|
||||||
"status": attempt_details,
|
"status": attempt_details.percentage
|
||||||
|
if assessment_type == "LMS Quiz"
|
||||||
|
else attempt_details.status,
|
||||||
"result": result,
|
"result": result,
|
||||||
|
"assessment": assessment,
|
||||||
|
"type": assessment_type,
|
||||||
|
"submission": attempt_details.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1586,11 +1628,19 @@ def get_order_summary(doctype, docname, country=None):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Course",
|
"LMS Course",
|
||||||
docname,
|
docname,
|
||||||
["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"],
|
[
|
||||||
|
"title",
|
||||||
|
"name",
|
||||||
|
"paid_course",
|
||||||
|
"paid_certificate",
|
||||||
|
"course_price as amount",
|
||||||
|
"currency",
|
||||||
|
"amount_usd",
|
||||||
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not details.paid_course:
|
if not details.paid_course and not details.paid_certificate:
|
||||||
raise frappe.throw(_("This course is free."))
|
raise frappe.throw(_("This course is free."))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -1704,9 +1754,14 @@ def update_payment_record(doctype, docname):
|
|||||||
"order_id": data.get("order_id"),
|
"order_id": data.get("order_id"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
payment_for_certificate = frappe.db.get_value(
|
||||||
|
"LMS Payment", data.payment, "payment_for_certificate"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if doctype == "LMS Course":
|
if payment_for_certificate:
|
||||||
|
update_certificate_purchase(docname)
|
||||||
|
elif doctype == "LMS Course":
|
||||||
enroll_in_course(data.payment, docname)
|
enroll_in_course(data.payment, docname)
|
||||||
else:
|
else:
|
||||||
enroll_in_batch(docname, data.payment)
|
enroll_in_batch(docname, data.payment)
|
||||||
@@ -1766,6 +1821,15 @@ def enroll_in_batch(batch, payment_name=None):
|
|||||||
new_student.save()
|
new_student.save()
|
||||||
|
|
||||||
|
|
||||||
|
def update_certificate_purchase(course):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Enrollment",
|
||||||
|
{"member": frappe.session.user, "course": course},
|
||||||
|
"purchased_certificate",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_programs():
|
def get_programs():
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
{% set chapters = get_chapters(course.name) %}
|
|
||||||
{% set is_instructor = is_instructor(course.name) %}
|
|
||||||
|
|
||||||
{% if chapters | length %}
|
|
||||||
<div class="course-home-outline">
|
|
||||||
|
|
||||||
{% if not lesson_page %}
|
|
||||||
<div class="page-title mb-8" id="outline-heading" data-course="{{ course.name }}">
|
|
||||||
{{ _("Course Content") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <div class="mb-2">
|
|
||||||
<span>
|
|
||||||
{{ chapters | length }} chapters
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
. {{ get_lessons(course.name, None, False) }} lessons
|
|
||||||
</span>
|
|
||||||
</div> -->
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if chapters | length %}
|
|
||||||
<div>
|
|
||||||
{% for chapter in chapters %}
|
|
||||||
{% set lessons = get_lessons(course.name, chapter) %}
|
|
||||||
|
|
||||||
<div class="chapter-parent" data-chapter="{{ chapter.name }}">
|
|
||||||
|
|
||||||
<div class="chapter-title" data-toggle="collapse" aria-expanded="false"
|
|
||||||
data-target="#{{ get_slugified_chapter_title(chapter.title) }}">
|
|
||||||
|
|
||||||
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
|
|
||||||
<div class="chapter-title-main">
|
|
||||||
{{ chapter.title }}
|
|
||||||
</div>
|
|
||||||
<!-- <div class="small ml-auto">
|
|
||||||
{{ lessons | length }} lessons
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
|
|
||||||
|
|
||||||
{% if chapter.description %}
|
|
||||||
<div class="chapter-description">
|
|
||||||
{{ chapter.description }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="lessons">
|
|
||||||
|
|
||||||
{% if lessons | length %}
|
|
||||||
|
|
||||||
{% for lesson in lessons %}
|
|
||||||
{% set active = membership.current_lesson == lesson.name %}
|
|
||||||
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active %} active-lesson {% endif %}">
|
|
||||||
|
|
||||||
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
|
|
||||||
<a class="lesson-links"
|
|
||||||
href="{{ get_lesson_url(course.name, lesson.number) }}{% if classname %}?class={{ classname }}{% endif %}{{course.query_parameter}}"
|
|
||||||
{% if is_instructor and not lesson.include_in_preview %}
|
|
||||||
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
|
||||||
{% endif %}>
|
|
||||||
|
|
||||||
<svg class="icon icon-sm mr-2">
|
|
||||||
<use class="" href="#{{ lesson.icon }}">
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span>{{ lesson.title }}</span>
|
|
||||||
|
|
||||||
{% if membership %}
|
|
||||||
<svg class="icon icon-md lesson-progress-tick ml-auto {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
|
|
||||||
<use class="" href="#icon-success">
|
|
||||||
</svg>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<div class="no-preview" title="This lesson is not available for preview">
|
|
||||||
<div class="lesson-links">
|
|
||||||
<svg class="icon icon-sm mr-2">
|
|
||||||
<use class="" href="#icon-lock-gray">
|
|
||||||
</svg>
|
|
||||||
<div>{{ lesson.title }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if chapters | length %}
|
|
||||||
<!-- No Preview Modal -->
|
|
||||||
{{ widgets.NoPreviewModal(course=course, membership=membership) }}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
685
lms/locale/ar.po
685
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
1015
lms/locale/bs.po
1015
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
687
lms/locale/de.po
687
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
691
lms/locale/eo.po
691
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
691
lms/locale/es.po
691
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
711
lms/locale/fa.po
711
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
687
lms/locale/fr.po
687
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
685
lms/locale/hu.po
685
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
685
lms/locale/pl.po
685
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
689
lms/locale/ru.po
689
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
695
lms/locale/sv.po
695
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
687
lms/locale/tr.po
687
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
685
lms/locale/zh.po
685
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -99,4 +99,6 @@ lms.patches.v2_0.update_quiz_submission_data
|
|||||||
lms.patches.v2_0.convert_quiz_duration_to_minutes
|
lms.patches.v2_0.convert_quiz_duration_to_minutes
|
||||||
lms.patches.v2_0.allow_guest_access #05-02-2025
|
lms.patches.v2_0.allow_guest_access #05-02-2025
|
||||||
lms.patches.v2_0.migrate_batch_student_data #10-02-2025
|
lms.patches.v2_0.migrate_batch_student_data #10-02-2025
|
||||||
lms.patches.v2_0.delete_old_enrollment_doctypes
|
lms.patches.v2_0.delete_old_enrollment_doctypes
|
||||||
|
lms.patches.v2_0.delete_unused_custom_fields
|
||||||
|
lms.patches.v2_0.update_certificate_request_status
|
||||||
24
lms/patches/v2_0/delete_unused_custom_fields.py
Normal file
24
lms/patches/v2_0/delete_unused_custom_fields.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if "payments" not in frappe.get_installed_apps():
|
||||||
|
web_form_custom_fields = frappe.get_all(
|
||||||
|
"Custom Field", {"dt": "Web Form"}, ["name", "fieldname"]
|
||||||
|
)
|
||||||
|
|
||||||
|
unused_fields = [
|
||||||
|
"currency",
|
||||||
|
"amount_field",
|
||||||
|
"amount_based_on_field",
|
||||||
|
"payment_button_help",
|
||||||
|
"amount",
|
||||||
|
"payments_cb",
|
||||||
|
"payment_button_label",
|
||||||
|
"payment_gateway",
|
||||||
|
"payments_tab",
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in web_form_custom_fields:
|
||||||
|
if field.fieldname in unused_fields:
|
||||||
|
frappe.delete_doc("Custom Field", field.name)
|
||||||
14
lms/patches/v2_0/update_certificate_request_status.py
Normal file
14
lms/patches/v2_0/update_certificate_request_status.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.utils import getdate
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
evaluations = frappe.get_all("LMS Certificate Request", fields=["name", "date"])
|
||||||
|
|
||||||
|
for evaluation in evaluations:
|
||||||
|
if evaluation.date > getdate():
|
||||||
|
frappe.db.set_value("LMS Certificate Request", evaluation.name, "status", "Upcoming")
|
||||||
|
else:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Certificate Request", evaluation.name, "status", "Completed"
|
||||||
|
)
|
||||||
@@ -6,12 +6,6 @@
|
|||||||
{{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }}
|
{{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }}
|
||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<p>
|
|
||||||
<b>
|
|
||||||
{{ title }}
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
|
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
|
||||||
@@ -27,7 +21,7 @@
|
|||||||
<br>
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{{ _("Visit the following link to view your ") }}
|
{{ _("Visit the following link to view your ") }}
|
||||||
<a href="/batches/{{ name }}">{{ _("Batch Details") }}</a>
|
<a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
|
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
|
||||||
|
|||||||
35
lms/templates/emails/batch_start_reminder.html
Normal file
35
lms/templates/emails/batch_start_reminder.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<p>
|
||||||
|
{{ _("Dear ") }} {{ student_name }},
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("The batch you have enrolled for is starting tomorrow. Please be prepared and be on time for the session.") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Batch:") }}</b> {{ title }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Medium:") }}</b> {{ medium }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("Visit the following link to view your ") }}
|
||||||
|
<a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("Best Regards") }}
|
||||||
|
</p>
|
||||||
31
lms/templates/emails/live_class_reminder.html
Normal file
31
lms/templates/emails/live_class_reminder.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<p>
|
||||||
|
{{ _("Dear ") }} {{ student_name }},
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("You have a live class scheduled tomorrow. Please be prepared and be on time for the session.") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Class:") }}</b> {{ title }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Date:") }}</b> {{ frappe.utils.format_date(date, "medium") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(time, "hh:mm a") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("Visit the following link to view your ") }}
|
||||||
|
<a href="/lms/live_classes/{{ batch_name }}">{{ _("Batch Details") }}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("Best Regards") }}
|
||||||
|
</p>
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>{{ _('Hi') }} {{ billing_name }},</p>
|
<p>{{ _('Hi') }} {{ billing_name }},</p>
|
||||||
|
<br>
|
||||||
<p>{{ _('We noticed that you started enrolling in the') }} {{ type }} {{ title }} {{ _('but didn’t complete your payment') }}.</p>
|
<p>{{ _('We noticed that you started enrolling in the') }} {{ type }} {{ title }} {{ _('but didn’t complete your payment') }}.</p>
|
||||||
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{{ _("We have a limited number of seats, and they won't be available for long!")}}
|
{{ _("We have a limited number of seats, and they won't be available for long!")}}
|
||||||
</p>
|
</p>
|
||||||
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{{ _("Don’t miss this opportunity to enhance your skills. Click below to complete your enrollment") }}:
|
{{ _("Don’t miss this opportunity to enhance your skills. Click below to complete your enrollment") }}:
|
||||||
</p>
|
</p>
|
||||||
|
<br>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ link }}">👉 Complete Your Enrollment</a>
|
<a href="{{ link }}">👉 {{ _("Complete Your Enrollment") }}</a>
|
||||||
</p>
|
</p>
|
||||||
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{{ _("If you have any questions or need assistance, feel free to reach out to our support team.") }}
|
{{ _("If you have any questions or need assistance, feel free to reach out to our support team.") }}
|
||||||
</p>
|
</p>
|
||||||
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{{ _("Looking forward to seeing you enrolled!") }}
|
{{ _("Looking forward to seeing you enrolled!") }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils.telemetry import capture
|
|
||||||
from frappe import _
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import re
|
import re
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils.telemetry import capture
|
||||||
|
from frappe.utils import cint
|
||||||
|
|
||||||
no_cache = 1
|
no_cache = 1
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ def get_context():
|
|||||||
csrf_token = frappe.sessions.get_csrf_token()
|
csrf_token = frappe.sessions.get_csrf_token()
|
||||||
frappe.db.commit() # nosemgrep
|
frappe.db.commit() # nosemgrep
|
||||||
context.csrf_token = csrf_token
|
context.csrf_token = csrf_token
|
||||||
|
context.setup_complete = cint(frappe.get_system_settings("setup_complete"))
|
||||||
capture("active_site", "lms")
|
capture("active_site", "lms")
|
||||||
context.favicon = frappe.db.get_single_value("Website Settings", "favicon")
|
context.favicon = frappe.db.get_single_value("Website Settings", "favicon")
|
||||||
return context
|
return context
|
||||||
|
|||||||
Reference in New Issue
Block a user