Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b3906359b | ||
|
|
4224580d6f | ||
|
|
07d30647d8 | ||
|
|
263096fc77 | ||
|
|
b510cbce7f | ||
|
|
0b84dc3266 | ||
|
|
7ee7b95eb5 | ||
|
|
83b8bdde45 | ||
|
|
1b5dd15b90 | ||
|
|
47c224fcad | ||
|
|
1c866f40eb | ||
|
|
1861aabaca | ||
|
|
cd8fb6eb38 | ||
|
|
21d05d3731 | ||
|
|
7c953925f9 | ||
|
|
33a4bbbe47 | ||
|
|
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 | ||
|
|
03915ccfbd | ||
|
|
c6d59216fd | ||
|
|
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 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
node-version: '18'
|
||||
check-latest: true
|
||||
- name: setup cache for bench
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/bench-cache
|
||||
key: ${{ runner.os }}
|
||||
|
||||
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
||||
openMode: 0,
|
||||
},
|
||||
e2e: {
|
||||
baseUrl: "http://lms1:8000",
|
||||
baseUrl: "http://testui:8000",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ describe("Course Creation", () => {
|
||||
cy.visit("/lms/courses");
|
||||
|
||||
// Create a course
|
||||
cy.get("header").children().last().children().last().click();
|
||||
cy.get("button").contains("New").click();
|
||||
cy.wait(1000);
|
||||
cy.url().should("include", "/courses/new/edit");
|
||||
|
||||
@@ -84,9 +84,8 @@ describe("Course Creation", () => {
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get("label").contains("Title").type("Test Lesson");
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
<script>
|
||||
window.csrf_token = '{{ csrf_token }}'
|
||||
window.setup_complete = '{{ setup_complete }}'
|
||||
document.getElementById('seo-content').style.display = 'none';
|
||||
</script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
"@editorjs/paragraph": "^2.11.3",
|
||||
"@editorjs/simple-image": "^1.6.0",
|
||||
"@editorjs/table": "^2.4.2",
|
||||
"@vueuse/router": "^12.7.0",
|
||||
"ace-builds": "^1.36.2",
|
||||
"apexcharts": "^4.3.0",
|
||||
"chart.js": "^4.4.1",
|
||||
"codemirror-editor-vue3": "^2.8.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.109",
|
||||
"frappe-ui": "^0.1.112",
|
||||
"lucide-vue-next": "^0.383.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"pinia": "^2.0.33",
|
||||
|
||||
@@ -62,25 +62,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarLink
|
||||
:link="{
|
||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||
}"
|
||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
@click="toggleSidebar()"
|
||||
class="m-2"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
<CollapseSidebar
|
||||
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
|
||||
:class="{
|
||||
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SidebarLink>
|
||||
<div>
|
||||
<TrialBanner
|
||||
v-if="
|
||||
userResource.data?.is_system_manager && userResource.data?.is_fc_site
|
||||
"
|
||||
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
/>
|
||||
<SidebarLink
|
||||
:link="{
|
||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||
}"
|
||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
@click="toggleSidebar()"
|
||||
class="m-2"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
<CollapseSidebar
|
||||
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
|
||||
:class="{
|
||||
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SidebarLink>
|
||||
</div>
|
||||
</div>
|
||||
<PageModal
|
||||
v-model="showPageModal"
|
||||
@@ -101,7 +109,7 @@ import { sessionStore } from '@/stores/session'
|
||||
import { useSidebar } from '@/stores/sidebar'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
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'
|
||||
|
||||
const { user, sidebarSettings } = sessionStore()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
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 }"
|
||||
>
|
||||
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]">
|
||||
@@ -81,8 +81,8 @@
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else>
|
||||
<div class="flex items-center text-ink-gray-7">
|
||||
<div class="border rounded-md p-2 mr-2">
|
||||
<div class="flex text-ink-gray-7">
|
||||
<div class="border self-start rounded-md p-2 mr-2">
|
||||
<FileText class="h-5 w-5 stroke-1.5" />
|
||||
</div>
|
||||
<a
|
||||
@@ -90,7 +90,7 @@
|
||||
target="_blank"
|
||||
class="flex flex-col cursor-pointer !no-underline"
|
||||
>
|
||||
<span>
|
||||
<span class="text-sm leading-5">
|
||||
{{ submissionFile.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-ink-gray-5 mt-1">
|
||||
@@ -155,12 +155,23 @@
|
||||
type="select"
|
||||
:options="submissionStatusOptions"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="submissionResource.doc"
|
||||
v-model="submissionResource.doc.comments"
|
||||
:label="__('Comments')"
|
||||
type="textarea"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm text-ink-gray-5 mb-1">
|
||||
{{ __('Comments') }}
|
||||
</div>
|
||||
<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>
|
||||
@@ -184,6 +195,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
const submissionFile = ref(null)
|
||||
const answer = ref(null)
|
||||
const comments = ref(null)
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
|
||||
@@ -281,6 +293,9 @@ watch(submissionResource, () => {
|
||||
if (submissionResource.doc.answer) {
|
||||
answer.value = submissionResource.doc.answer
|
||||
}
|
||||
if (submissionResource.doc.comments) {
|
||||
comments.value = submissionResource.doc.comments
|
||||
}
|
||||
if (submissionResource.isDirty) {
|
||||
isDirty.value = true
|
||||
} else if (showUploader() && !submissionFile.value) {
|
||||
@@ -305,11 +320,14 @@ const submitAssignment = () => {
|
||||
submissionResource.doc && submissionResource.doc.owner != user.data?.name
|
||||
? user.data?.name
|
||||
: null
|
||||
|
||||
submissionResource.setValue.submit(
|
||||
{
|
||||
...submissionResource.doc,
|
||||
assignment_attachment: submissionFile.value?.file_url,
|
||||
evaluator: evaluator,
|
||||
comments: comments.value,
|
||||
answer: answer.value,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<template #prefix>
|
||||
<div v-if="column.key == 'member_name'">
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
class="flex"
|
||||
:image="row['member_image']"
|
||||
:label="item"
|
||||
size="sm"
|
||||
@@ -240,6 +240,6 @@ const feedbackColumns = computed(() => {
|
||||
<style>
|
||||
.feedback-list > button > div {
|
||||
align-items: start;
|
||||
padding: 0.25rem 0;
|
||||
padding: 0.15rem 0;
|
||||
}
|
||||
</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.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 }}
|
||||
<span v-if="seats_left > 1">
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div
|
||||
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') }}
|
||||
</div>
|
||||
@@ -69,7 +69,11 @@
|
||||
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">
|
||||
<span>
|
||||
@@ -80,7 +84,11 @@
|
||||
<Button
|
||||
variant="solid"
|
||||
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()"
|
||||
>
|
||||
{{ __('Enroll Now') }}
|
||||
@@ -112,6 +120,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
:options="chartOptions"
|
||||
:series="chartData"
|
||||
type="bar"
|
||||
height="200"
|
||||
:height="chartData[0].data.length * 30 + 100"
|
||||
/>
|
||||
<div
|
||||
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) => {
|
||||
if (student.assessments[assessment].result === 'Passed') {
|
||||
if (student.assessments[assessment].result === 'Pass') {
|
||||
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>
|
||||
@@ -130,7 +130,7 @@ import {
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
} from '@headlessui/vue'
|
||||
import { Popover, Button } from 'frappe-ui'
|
||||
import { Popover } from 'frappe-ui'
|
||||
import { ChevronDown, X } from 'lucide-vue-next'
|
||||
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
|
||||
|
||||
|
||||
@@ -100,9 +100,15 @@
|
||||
<CourseInstructors :instructors="course.instructors" />
|
||||
</div>
|
||||
|
||||
<div class="font-semibold">
|
||||
<div v-if="course.paid_course" class="font-semibold">
|
||||
{{ course.price }}
|
||||
</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>
|
||||
|
||||
@@ -6,30 +6,32 @@
|
||||
class="rounded-t-md min-h-56 w-full"
|
||||
/>
|
||||
<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 }}
|
||||
</div>
|
||||
<router-link
|
||||
v-if="course.data.membership"
|
||||
:to="{
|
||||
name: 'Lesson',
|
||||
params: {
|
||||
courseName: course.name,
|
||||
chapterNumber: course.data.current_lesson
|
||||
? course.data.current_lesson.split('-')[0]
|
||||
: 1,
|
||||
lessonNumber: course.data.current_lesson
|
||||
? course.data.current_lesson.split('-')[1]
|
||||
: 1,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="solid" size="md" class="w-full">
|
||||
<span>
|
||||
{{ __('Continue Learning') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<div v-if="course.data.membership" class="space-y-2">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Lesson',
|
||||
params: {
|
||||
courseName: course.name,
|
||||
chapterNumber: course.data.current_lesson
|
||||
? course.data.current_lesson.split('-')[0]
|
||||
: 1,
|
||||
lessonNumber: course.data.current_lesson
|
||||
? course.data.current_lesson.split('-')[1]
|
||||
: 1,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button variant="solid" size="md" class="w-full">
|
||||
<span>
|
||||
{{ __('Continue Learning') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
<CertificationLinks :courseName="course.data.name" />
|
||||
</div>
|
||||
<router-link
|
||||
v-else-if="course.data.paid_course"
|
||||
:to="{
|
||||
@@ -113,17 +115,36 @@
|
||||
{{ course.data.rating }} {{ __('Rating') }}
|
||||
</span>
|
||||
</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>
|
||||
</template>
|
||||
<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 { Button, createResource } from 'frappe-ui'
|
||||
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||
import { showToast, formatAmount } from '@/utils/'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="flex mt-2">
|
||||
<Star
|
||||
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="
|
||||
index <= Math.ceil(review.rating)
|
||||
? 'fill-orange-500'
|
||||
|
||||
23
frontend/src/components/Icons/FrappeCloudIcon.vue
Normal file
23
frontend/src/components/Icons/FrappeCloudIcon.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="0.75"
|
||||
y="0.75"
|
||||
width="30.5"
|
||||
height="30.5"
|
||||
rx="6.25"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M24.5011 14.1124C23.3954 12.4873 21.532 11.5477 19.594 11.6747C18.7616 10.1384 17.2211 9.12267 15.4198 9.0084C14.1651 8.93222 12.8979 9.3766 11.9165 10.24C11.2456 10.8367 10.7611 11.5223 10.463 12.2968C10.289 12.7539 9.89151 13.0459 9.46912 13.0459H6.5V15.5852H9.46912C10.9226 15.5852 12.2271 14.6584 12.7737 13.2237C12.9227 12.8301 13.1712 12.4873 13.5439 12.1571C14.0284 11.7255 14.662 11.4969 15.2583 11.535C16.1528 11.5985 16.7863 12.0175 17.1839 12.538C17.6063 13.0205 17.8423 13.7696 17.979 14.5187C18.774 14.2902 19.6437 14.0997 20.476 14.2394C21.1593 14.3536 21.7929 14.7218 22.2525 15.2678C22.327 15.3567 22.4016 15.4456 22.4637 15.5471C23.06 16.4232 23.1718 17.5024 22.7743 18.5689C22.414 19.5592 21.0847 20.4607 19.9791 20.4607H11.3326C10.1524 20.4607 9.18339 19.5592 9.03432 18.4038H6.54969C6.71119 20.9686 8.78585 23 11.3326 23H19.9915C22.1283 23 24.3769 21.451 25.1098 19.4704C25.7931 17.6167 25.5695 15.6614 24.5135 14.0997L24.5011 14.1124Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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 items-center justify-between">
|
||||
<span class="font-semibold text-ink-gray-9">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<FormControl
|
||||
v-model="member.first_name"
|
||||
:placeholder="__('First Name')"
|
||||
type="test"
|
||||
type="text"
|
||||
class="w-full"
|
||||
/>
|
||||
<Button @click="addMember()" variant="subtle">
|
||||
|
||||
@@ -67,13 +67,16 @@ const announcement = reactive({
|
||||
})
|
||||
|
||||
const announcementResource = createResource({
|
||||
url: 'lms.lms.api.make_announcement',
|
||||
url: 'frappe.core.doctype.communication.email.make',
|
||||
makeParams(values) {
|
||||
return {
|
||||
students: props.students,
|
||||
recipients: props.students.join(', '),
|
||||
cc: announcement.replyTo,
|
||||
subject: announcement.subject,
|
||||
content: announcement.announcement,
|
||||
doctype: 'LMS Batch',
|
||||
name: props.batch,
|
||||
send_email: 1,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource } from 'frappe-ui'
|
||||
import { ref, defineModel } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
|
||||
@@ -32,25 +32,44 @@
|
||||
{{ __('Assessment') }}
|
||||
</span>
|
||||
<span>
|
||||
{{ __('Progress') }}
|
||||
{{ __('Percentage/Status') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
<router-link
|
||||
v-for="assessment in Object.keys(student.assessments)"
|
||||
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">
|
||||
{{ assessment }}
|
||||
</span>
|
||||
<span v-if="isAssignment(student.assessments[assessment])">
|
||||
<Badge :theme="getStatusTheme(student.assessments[assessment])">
|
||||
{{ student.assessments[assessment] }}
|
||||
<span v-if="isAssignment(student.assessments[assessment].status)">
|
||||
<Badge
|
||||
:theme="
|
||||
getStatusTheme(student.assessments[assessment].status)
|
||||
"
|
||||
>
|
||||
{{ student.assessments[assessment].status }}
|
||||
</Badge>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ student.assessments[assessment] }}
|
||||
{{ student.assessments[assessment].status }}
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Courses -->
|
||||
|
||||
@@ -77,7 +77,7 @@ import {
|
||||
FormControl,
|
||||
Switch,
|
||||
} from 'frappe-ui'
|
||||
import { defineModel, reactive, watch } from 'vue'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { showToast, getFileSize } from '@/utils/'
|
||||
import { capture } from '@/telemetry'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
||||
import { reactive, defineModel } from 'vue'
|
||||
import { reactive } from 'vue'
|
||||
import { showToast, singularize } from '@/utils'
|
||||
|
||||
const topics = defineModel('reloadTopics')
|
||||
|
||||
@@ -94,7 +94,7 @@ import {
|
||||
createResource,
|
||||
TextEditor,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, watch, defineModel } from 'vue'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { getFileSize, showToast, escapeHTML } from '@/utils'
|
||||
|
||||
|
||||
@@ -25,7 +25,15 @@
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Date') }}
|
||||
</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 v-if="slots.data?.length">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
@@ -58,7 +66,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
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/'
|
||||
|
||||
const user = inject('$user')
|
||||
@@ -161,6 +169,11 @@ const getCourses = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (courses.length == 1) {
|
||||
evaluation.course = courses[0].value
|
||||
}
|
||||
|
||||
return courses
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<script setup>
|
||||
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
|
||||
import { FileText } from 'lucide-vue-next'
|
||||
import { ref, inject, defineModel } from 'vue'
|
||||
import { ref, inject } from 'vue'
|
||||
import { createToast, getFileSize } from '@/utils/'
|
||||
|
||||
const resume = ref(null)
|
||||
|
||||
@@ -39,13 +39,19 @@
|
||||
:required="true"
|
||||
/>
|
||||
</Tooltip>
|
||||
<FormControl
|
||||
v-model="liveClass.timezone"
|
||||
type="select"
|
||||
:options="getTimezoneOptions()"
|
||||
:label="__('Timezone')"
|
||||
:required="true"
|
||||
/>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-ink-gray-5 text-xs" for="batchTimezone">
|
||||
{{ __('Timezone') }}
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</label>
|
||||
<Autocomplete
|
||||
@update:modelValue="(opt) => (liveClass.timezone = opt.value)"
|
||||
:modelValue="liveClass.timezone"
|
||||
:options="getTimezoneOptions()"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
@@ -83,18 +89,14 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Input,
|
||||
DatePicker,
|
||||
Select,
|
||||
Textarea,
|
||||
Dialog,
|
||||
createResource,
|
||||
Tooltip,
|
||||
FormControl,
|
||||
Autocomplete,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, inject } from 'vue'
|
||||
import { getTimezones, createToast } from '@/utils/'
|
||||
import { Info } from 'lucide-vue-next'
|
||||
|
||||
const liveClasses = defineModel('reloadLiveClasses')
|
||||
const show = defineModel()
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
||||
import { defineModel, reactive } from 'vue'
|
||||
import { reactive } from 'vue'
|
||||
import Rating from '@/components/Controls/Rating.vue'
|
||||
import { createToast } from '@/utils/'
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="activeQuestion != questions.length"
|
||||
@click="nextQuetion()"
|
||||
@click="nextQuestion()"
|
||||
>
|
||||
<span>
|
||||
{{ __('Next') }}
|
||||
@@ -258,14 +258,22 @@
|
||||
</Button>
|
||||
</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"
|
||||
>
|
||||
<ListView
|
||||
:columns="getSubmissionColumns()"
|
||||
:rows="attempts?.data"
|
||||
row-key="name"
|
||||
:options="{ selectable: false, showTooltip: false }"
|
||||
:options="{
|
||||
selectable: false,
|
||||
showTooltip: false,
|
||||
emptyState: { title: __('No Quiz submissions found') },
|
||||
}"
|
||||
>
|
||||
</ListView>
|
||||
</div>
|
||||
@@ -282,7 +290,7 @@ import {
|
||||
FormControl,
|
||||
} from 'frappe-ui'
|
||||
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 { timeAgo } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -536,7 +544,7 @@ const addToLocalStorage = () => {
|
||||
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
|
||||
}
|
||||
|
||||
const nextQuetion = () => {
|
||||
const nextQuestion = () => {
|
||||
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
|
||||
checkAnswer()
|
||||
} else {
|
||||
@@ -574,6 +582,16 @@ const createSubmission = () => {
|
||||
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
||||
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">
|
||||
{{ __('Upcoming Evaluations') }}
|
||||
</div>
|
||||
<Button @click="openEvalModal">
|
||||
<Button
|
||||
v-if="
|
||||
!upcoming_evals.data?.length ||
|
||||
upcoming_evals.length == courses.length
|
||||
"
|
||||
@click="openEvalModal"
|
||||
>
|
||||
{{ __('Schedule Evaluation') }}
|
||||
</Button>
|
||||
</div>
|
||||
<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 class="border rounded-md p-3">
|
||||
<div class="font-semibold mb-3">
|
||||
@@ -28,17 +34,39 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<UserCog2 class="w-4 h-4 stroke-1.5" />
|
||||
<span class="ml-2 font-medium">
|
||||
<GraduationCap class="w-4 h-4 stroke-1.5" />
|
||||
<span class="ml-2">
|
||||
{{ evl.evaluator_name }}
|
||||
</span>
|
||||
</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 v-else class="text-sm italic text-ink-gray-5">
|
||||
{{ __('No upcoming evaluations.') }}
|
||||
{{ __('Please schedule an evaluation to get certified.') }}
|
||||
</div>
|
||||
</div>
|
||||
<EvaluationModal
|
||||
@@ -50,15 +78,23 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
|
||||
import { inject, ref } from 'vue'
|
||||
import {
|
||||
Ban,
|
||||
Calendar,
|
||||
Clock,
|
||||
GraduationCap,
|
||||
HeadsetIcon,
|
||||
} from 'lucide-vue-next'
|
||||
import { inject, ref, getCurrentInstance } from 'vue'
|
||||
import { formatTime } from '../utils'
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import { Button, createResource, call } from 'frappe-ui'
|
||||
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const user = inject('$user')
|
||||
const showEvalModal = ref(false)
|
||||
const app = getCurrentInstance()
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
@@ -77,10 +113,10 @@ const props = defineProps({
|
||||
|
||||
const upcoming_evals = createResource({
|
||||
url: 'lms.lms.utils.get_upcoming_evals',
|
||||
cache: ['upcoming_evals', user.data.name],
|
||||
params: {
|
||||
student: user.data.name,
|
||||
courses: props.courses.map((course) => course.course),
|
||||
batch: props.batch,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
@@ -88,4 +124,32 @@ const upcoming_evals = createResource({
|
||||
function openEvalModal() {
|
||||
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>
|
||||
|
||||
@@ -66,6 +66,14 @@ import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Dropdown } from 'frappe-ui'
|
||||
import Apps from '@/components/Apps.vue'
|
||||
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 { createDialog } from '@/utils/dialogs'
|
||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
||||
import FrappeCloudIcon from '@/components/Icons/FrappeCloudIcon.vue'
|
||||
import {
|
||||
ChevronDown,
|
||||
LogIn,
|
||||
@@ -75,12 +83,6 @@ import {
|
||||
Settings,
|
||||
Sun,
|
||||
} 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 { logout, branding } = sessionStore()
|
||||
@@ -89,6 +91,8 @@ const settingsStore = useSettings()
|
||||
let { isLoggedIn } = sessionStore()
|
||||
const showSettingsModal = ref(false)
|
||||
const theme = ref('light')
|
||||
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
|
||||
const $dialog = createDialog
|
||||
|
||||
const props = defineProps({
|
||||
isCollapsed: {
|
||||
@@ -130,6 +134,13 @@ const userDropdownOptions = computed(() => {
|
||||
return isLoggedIn
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: theme.value === 'light' ? Moon : Sun,
|
||||
label: 'Toggle Theme',
|
||||
onClick: () => {
|
||||
toggleTheme()
|
||||
},
|
||||
},
|
||||
{
|
||||
component: markRaw(Apps),
|
||||
condition: () => {
|
||||
@@ -139,13 +150,6 @@ const userDropdownOptions = computed(() => {
|
||||
else return false
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: theme.value === 'light' ? Moon : Sun,
|
||||
label: 'Toggle Theme',
|
||||
onClick: () => {
|
||||
toggleTheme()
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
label: 'Settings',
|
||||
@@ -156,6 +160,33 @@ const userDropdownOptions = computed(() => {
|
||||
return userResource.data?.is_moderator
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: FrappeCloudIcon,
|
||||
label: 'Login to Frappe Cloud',
|
||||
onClick: () => {
|
||||
$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) {
|
||||
loginToFrappeCloud()
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
condition: () => {
|
||||
return (
|
||||
userResource.data?.is_system_manager && userResource.data?.is_fc_site
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: LogOut,
|
||||
label: 'Log out',
|
||||
@@ -180,4 +211,12 @@ const userDropdownOptions = computed(() => {
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const loginToFrappeCloud = () => {
|
||||
let redirect_to = '/dashboard/welcome'
|
||||
if (userResource.data?.site_info.is_payment_method_added) {
|
||||
redirect_to = '/dashboard/sites/' + userResource.data.sitename
|
||||
}
|
||||
window.open(`${frappeCloudBaseEndpoint}${redirect_to}`, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button
|
||||
v-if="user.data?.is_moderator"
|
||||
v-if="user.data?.is_moderator && batch.data?.certification"
|
||||
@click="openCertificateDialog = true"
|
||||
>
|
||||
{{ __('Generate Certificates') }}
|
||||
@@ -21,7 +21,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
</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">
|
||||
<Tabs
|
||||
v-model="tabIndex"
|
||||
@@ -190,8 +193,9 @@
|
||||
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
||||
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 UserAvatar from '@/components/UserAvatar.vue'
|
||||
import {
|
||||
@@ -267,7 +271,7 @@ const isStudent = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const tabIndex = useRouteQuery('tab', 0, { transform: Number })
|
||||
const tabs = computed(() => {
|
||||
let batchTabs = []
|
||||
batchTabs.push({
|
||||
@@ -310,7 +314,7 @@ const tabs = computed(() => {
|
||||
})
|
||||
|
||||
const redirectToLogin = () => {
|
||||
window.location.href = `/login?redirect-to=/batches`
|
||||
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
|
||||
}
|
||||
|
||||
const openAnnouncementModal = () => {
|
||||
|
||||
@@ -127,6 +127,11 @@ const batch = createResource({
|
||||
batch: props.batchName,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess: (data) => {
|
||||
if (!data) {
|
||||
router.push({ name: 'Batches' })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const courses = createResource({
|
||||
|
||||
@@ -13,15 +13,14 @@
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Details') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="space-y-4 mb-4">
|
||||
<FormControl
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="flex items-center space-x-5">
|
||||
<FormControl
|
||||
v-model="batch.published"
|
||||
type="checkbox"
|
||||
@@ -32,6 +31,11 @@
|
||||
type="checkbox"
|
||||
:label="__('Allow self enrollment')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.certification"
|
||||
type="checkbox"
|
||||
:label="__('Certification')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,30 +94,8 @@
|
||||
:required="true"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<FormControl
|
||||
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="my-10">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Date and Time') }}
|
||||
</div>
|
||||
@@ -133,6 +115,14 @@
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.timezone"
|
||||
:label="__('Timezone')"
|
||||
type="text"
|
||||
:placeholder="__('Example: IST (+5:30)')"
|
||||
class="mb-4"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
@@ -149,18 +139,11 @@
|
||||
class="mb-4"
|
||||
: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 class="mb-4">
|
||||
|
||||
<div class="mb-10">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
@@ -179,6 +162,11 @@
|
||||
type="date"
|
||||
class="mb-4"
|
||||
/>
|
||||
<Link
|
||||
doctype="Email Template"
|
||||
:label="__('Email Template')"
|
||||
v-model="batch.confirmation_email_template"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
@@ -230,6 +218,33 @@
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
@@ -278,10 +293,12 @@ const batch = reactive({
|
||||
end_time: '',
|
||||
timezone: '',
|
||||
evaluation_end_date: '',
|
||||
confirmation_email_template: '',
|
||||
seat_count: '',
|
||||
medium: '',
|
||||
category: '',
|
||||
allow_self_enrollment: false,
|
||||
certification: false,
|
||||
image: null,
|
||||
paid_batch: false,
|
||||
currency: '',
|
||||
@@ -351,7 +368,12 @@ const batchDetail = createResource({
|
||||
batch[key] = `${hours}:${minutes}`
|
||||
} 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) {
|
||||
let key = checkboxes[idx]
|
||||
batch[key] = batch[key] ? true : false
|
||||
|
||||
@@ -26,13 +26,19 @@
|
||||
{{ __('All Batches') }}
|
||||
</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
|
||||
v-if="user.data"
|
||||
:buttons="batchTabs"
|
||||
v-model="currentTab"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="certification"
|
||||
:label="__('Certification')"
|
||||
type="checkbox"
|
||||
@change="updateBatches()"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<FormControl
|
||||
v-model="title"
|
||||
@@ -111,6 +117,7 @@ const pageLength = ref(20)
|
||||
const categories = ref([])
|
||||
const currentCategory = ref(null)
|
||||
const title = ref('')
|
||||
const certification = ref(false)
|
||||
const filters = ref({})
|
||||
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
|
||||
const orderBy = ref('start_date')
|
||||
@@ -130,6 +137,7 @@ const setFiltersFromQuery = () => {
|
||||
let queries = new URLSearchParams(location.search)
|
||||
title.value = queries.get('title') || ''
|
||||
currentCategory.value = queries.get('category') || null
|
||||
certification.value = queries.get('certification') || false
|
||||
}
|
||||
|
||||
const batches = createListResource({
|
||||
@@ -161,6 +169,7 @@ const updateBatches = () => {
|
||||
const updateFilters = () => {
|
||||
updateCategoryFilter()
|
||||
updateTitleFilter()
|
||||
updateCertificationFilter()
|
||||
updateTabFilter()
|
||||
updateStudentFilter()
|
||||
setQueryParams()
|
||||
@@ -182,6 +191,14 @@ const updateTitleFilter = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateCertificationFilter = () => {
|
||||
if (certification.value) {
|
||||
filters.value['certification'] = 1
|
||||
} else {
|
||||
delete filters.value['certification']
|
||||
}
|
||||
}
|
||||
|
||||
const updateTabFilter = () => {
|
||||
orderBy.value = 'start_date'
|
||||
if (!user.data) {
|
||||
@@ -222,6 +239,7 @@ const setQueryParams = () => {
|
||||
let filterKeys = {
|
||||
title: title.value,
|
||||
category: currentCategory.value,
|
||||
certification: certification.value,
|
||||
}
|
||||
|
||||
Object.keys(filterKeys).forEach((key) => {
|
||||
|
||||
@@ -12,20 +12,15 @@
|
||||
v-if="access.data?.access && orderSummary.data"
|
||||
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="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">
|
||||
{{ __('Ordered Item') }}
|
||||
{{ __('Payment for ') }} {{ type }}:
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="leading-5">
|
||||
{{ orderSummary.data.title }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,7 +121,7 @@
|
||||
<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>
|
||||
@@ -140,10 +135,10 @@
|
||||
<div v-else-if="access.data?.message">
|
||||
<NotPermitted
|
||||
:text="access.data.message"
|
||||
:buttonLabel="
|
||||
type == 'course' ? 'Checkout Courses' : 'Checkout Batches'
|
||||
:buttonLabel="type == 'course' ? 'Checkout Course' : 'Checkout Batch'"
|
||||
:buttonLink="
|
||||
type == 'course' ? `/lms/courses/${name}` : `/lms/batches/${name}`
|
||||
"
|
||||
:buttonLink="type == 'course' ? '/lms/courses' : '/lms/batches'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!user.data?.name">
|
||||
@@ -163,7 +158,7 @@ import {
|
||||
Breadcrumbs,
|
||||
Tooltip,
|
||||
} 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 NotPermitted from '@/components/NotPermitted.vue'
|
||||
import { showToast } from '@/utils/'
|
||||
@@ -193,7 +188,7 @@ const props = defineProps({
|
||||
const access = createResource({
|
||||
url: 'lms.lms.api.validate_billing_access',
|
||||
params: {
|
||||
type: props.type,
|
||||
billing_type: props.type,
|
||||
name: props.name,
|
||||
},
|
||||
onSuccess(data) {
|
||||
@@ -206,7 +201,7 @@ const orderSummary = createResource({
|
||||
url: 'lms.lms.utils.get_order_summary',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
||||
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||
docname: props.name,
|
||||
country: billingDetails.country,
|
||||
}
|
||||
@@ -236,22 +231,26 @@ const paymentLink = createResource({
|
||||
url: 'lms.lms.payments.get_payment_link',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
||||
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||
docname: props.name,
|
||||
title: orderSummary.data.title,
|
||||
amount: orderSummary.data.original_amount,
|
||||
total_amount: orderSummary.data.amount,
|
||||
currency: orderSummary.data.currency,
|
||||
address: billingDetails,
|
||||
redirect_to: redirectTo.value,
|
||||
payment_for_certificate: props.type == 'certificate',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const generatePaymentLink = () => {
|
||||
console.log('called')
|
||||
paymentLink.submit(
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
console.log('validation start')
|
||||
if (!billingDetails.source) {
|
||||
return __('Please let us know where you heard about us from.')
|
||||
}
|
||||
@@ -330,6 +329,8 @@ const validateAddress = () => {
|
||||
!states.includes(billingDetails.state)
|
||||
)
|
||||
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
|
||||
|
||||
console.log('validation address')
|
||||
}
|
||||
|
||||
const showError = (err) => {
|
||||
@@ -347,4 +348,14 @@ const changeCurrency = (country) => {
|
||||
billingDetails.country = country
|
||||
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>
|
||||
|
||||
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">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-10 mb-4">
|
||||
<div class="grid grid-cols-2 gap-10 mb-4">
|
||||
<div
|
||||
v-if="user.data?.is_moderator"
|
||||
class="flex flex-col space-y-4"
|
||||
@@ -188,43 +188,48 @@
|
||||
v-model="course.featured"
|
||||
:label="__('Featured')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.disable_self_learning"
|
||||
:label="__('Disable Self Enrollment')"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.enable_certification"
|
||||
:label="__('Completion Certificate')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container border-t">
|
||||
<div class="text-lg font-semibold mt-5 mb-4">
|
||||
{{ __('Pricing') }}
|
||||
<div class="container border-t space-y-4">
|
||||
<div class="text-lg font-semibold mt-5">
|
||||
{{ __('Pricing and Certification') }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="grid grid-cols-3">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.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>
|
||||
<FormControl
|
||||
v-model="course.course_price"
|
||||
:label="__('Course Price')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<FormControl v-model="course.course_price" :label="__('Amount')" />
|
||||
<Link
|
||||
doctype="Currency"
|
||||
v-model="course.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
/>
|
||||
<Link
|
||||
v-if="course.paid_certificate"
|
||||
doctype="Course Evaluator"
|
||||
v-model="course.evaluator"
|
||||
:label="__('Evaluator')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,8 +301,10 @@ const course = reactive({
|
||||
disable_self_learning: false,
|
||||
enable_certification: false,
|
||||
paid_course: false,
|
||||
paid_certificate: false,
|
||||
course_price: '',
|
||||
currency: '',
|
||||
evaluator: '',
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -391,6 +398,7 @@ const courseResource = createResource({
|
||||
'paid_course',
|
||||
'featured',
|
||||
'enable_certification',
|
||||
'paid_certifiate',
|
||||
]
|
||||
for (let idx in checkboxes) {
|
||||
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"
|
||||
>
|
||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||
<CertificationLinks :courseName="courseName" />
|
||||
</header>
|
||||
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
||||
<div
|
||||
@@ -197,13 +198,14 @@ import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
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 { getEditorTools, updateDocumentTitle } from '../utils'
|
||||
import EditorJS from '@editorjs/editorjs'
|
||||
import LessonContent from '@/components/LessonContent.vue'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
|
||||
@@ -139,7 +139,7 @@ const renderEditor = (holder) => {
|
||||
const lesson = reactive({
|
||||
title: '',
|
||||
include_in_preview: false,
|
||||
body: 'Test',
|
||||
body: '',
|
||||
instructor_notes: '',
|
||||
content: '',
|
||||
})
|
||||
@@ -294,7 +294,7 @@ const convertToJSON = (lessonData) => {
|
||||
type: 'upload',
|
||||
data: {
|
||||
file_url: video,
|
||||
file_type: 'video',
|
||||
file_type: video.split('.').pop(),
|
||||
},
|
||||
})
|
||||
} else if (block.includes('{{ Audio')) {
|
||||
@@ -303,7 +303,7 @@ const convertToJSON = (lessonData) => {
|
||||
type: 'upload',
|
||||
data: {
|
||||
file_url: audio,
|
||||
file_type: 'audio',
|
||||
file_type: audio.split('.').pop(),
|
||||
},
|
||||
})
|
||||
} else if (block.includes('{{ PDF')) {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<FormControl
|
||||
type="number"
|
||||
v-model="quiz.max_attempts"
|
||||
:label="__('Maximun Attempts')"
|
||||
:label="__('Maximum Attempts')"
|
||||
/>
|
||||
<FormControl
|
||||
type="number"
|
||||
|
||||
@@ -28,6 +28,12 @@ const routes = [
|
||||
component: () => import('@/pages/Lesson.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/courses/:courseName/certification',
|
||||
name: 'CourseCertification',
|
||||
component: () => import('@/pages/CourseCertification.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/courses/:courseName/learn/:chapterName',
|
||||
name: 'SCORMChapter',
|
||||
@@ -219,7 +225,7 @@ let router = createRouter({
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const { userResource } = usersStore()
|
||||
const { isLoggedIn } = sessionStore()
|
||||
let { isLoggedIn } = sessionStore()
|
||||
const { allowGuestAccess } = useSettings()
|
||||
|
||||
try {
|
||||
|
||||
@@ -50,10 +50,18 @@ export class Markdown {
|
||||
this.wrapper.innerHTML = this.text
|
||||
|
||||
this.wrapper.addEventListener('keydown', (event) => {
|
||||
const value = event.target.textContent
|
||||
let value = event.target.textContent
|
||||
if (event.keyCode === 32 && value.startsWith('#')) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -75,7 +83,11 @@ export class Markdown {
|
||||
|
||||
parseContent(event) {
|
||||
event.preventDefault()
|
||||
const previousLine = this.wrapper.textContent
|
||||
let previousLine = this.wrapper.textContent
|
||||
if (event.keyCode === 190) {
|
||||
previousLine = previousLine + '.'
|
||||
}
|
||||
|
||||
if (previousLine && this.hasImage(previousLine)) {
|
||||
this.wrapper.textContent = ''
|
||||
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', {
|
||||
style: 'ordered',
|
||||
items: [
|
||||
{
|
||||
content: previousLine.replace('1. ', ''),
|
||||
content: previousLine.replace('1.', ''),
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -108,6 +120,10 @@ export class Markdown {
|
||||
this.convertBlock('embed', {
|
||||
source: previousLine,
|
||||
})
|
||||
} else {
|
||||
this.convertBlock('paragraph', {
|
||||
text: previousLine,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createApp, h } from 'vue'
|
||||
import { usersStore } from '../stores/user'
|
||||
import translationPlugin from '../translation'
|
||||
import { CircleHelp } from 'lucide-vue-next'
|
||||
import router from '@/router'
|
||||
|
||||
export class Quiz {
|
||||
constructor({ data, api, readOnly }) {
|
||||
@@ -46,6 +47,7 @@ export class Quiz {
|
||||
quiz: quiz,
|
||||
})
|
||||
app.use(translationPlugin)
|
||||
app.use(router)
|
||||
const { userResource } = usersStore()
|
||||
app.provide('$user', userResource)
|
||||
app.mount(this.wrapper)
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: ['fs'],
|
||||
allowedHosts: ['fs', 'bs'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
2441
frontend/yarn.lock
Normal file
2441
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
__version__ = "2.22.0"
|
||||
__version__ = "2.25.0"
|
||||
|
||||
@@ -951,62 +951,6 @@
|
||||
"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_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_on_submit": 0,
|
||||
@@ -1119,230 +1063,6 @@
|
||||
"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": "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_on_submit": 0,
|
||||
@@ -1399,174 +1119,6 @@
|
||||
"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": "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_on_submit": 0,
|
||||
@@ -1679,62 +1231,6 @@
|
||||
"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": "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_on_submit": 0,
|
||||
|
||||
@@ -116,6 +116,8 @@ scheduler_events = {
|
||||
"daily": [
|
||||
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
||||
"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",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
111
lms/lms/api.py
111
lms/lms/api.py
@@ -7,16 +7,12 @@ import zipfile
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
from frappe.translate import get_all_translations
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import (
|
||||
time_diff,
|
||||
now_datetime,
|
||||
get_datetime,
|
||||
getdate,
|
||||
cint,
|
||||
flt,
|
||||
now,
|
||||
@@ -24,10 +20,13 @@ from frappe.utils import (
|
||||
format_date,
|
||||
date_diff,
|
||||
)
|
||||
from typing import Optional
|
||||
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||
from xml.dom.minidom import parseString
|
||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||
from frappe.integrations.frappe_providers.frappecloud_billing import (
|
||||
is_fc_site,
|
||||
current_site_info,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -180,6 +179,11 @@ def get_user_info():
|
||||
user.is_moderator = "Moderator" in user.roles
|
||||
user.is_evaluator = "Batch Evaluator" in user.roles
|
||||
user.is_student = "LMS Student" in user.roles
|
||||
user.is_fc_site = is_fc_site()
|
||||
user.is_system_manager = "System Manager" in user.roles
|
||||
if user.is_fc_site and user.is_system_manager:
|
||||
user.site_info = current_site_info()
|
||||
user.sitename = frappe.local.site
|
||||
return user
|
||||
|
||||
|
||||
@@ -193,24 +197,24 @@ def get_translations():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_billing_access(type, name):
|
||||
def validate_billing_access(billing_type, name):
|
||||
access = True
|
||||
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":
|
||||
access = False
|
||||
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
|
||||
message = _("Module is incorrect.")
|
||||
|
||||
if not frappe.db.exists(doctype, name):
|
||||
if access and not frappe.db.exists(doctype, name):
|
||||
access = False
|
||||
message = _("Module Name is incorrect or does not exist.")
|
||||
|
||||
if type == "course":
|
||||
if access and billing_type == "course":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Enrollment", {"member": frappe.session.user, "course": name}
|
||||
)
|
||||
@@ -218,7 +222,7 @@ def validate_billing_access(type, name):
|
||||
access = False
|
||||
message = _("You are already enrolled for this course.")
|
||||
|
||||
else:
|
||||
elif access and billing_type == "batch":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
|
||||
)
|
||||
@@ -226,6 +230,19 @@ def validate_billing_access(type, name):
|
||||
access = False
|
||||
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",
|
||||
{"email_id": frappe.session.user},
|
||||
@@ -373,7 +390,7 @@ def get_evaluator_details(evaluator):
|
||||
|
||||
|
||||
@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 = {}
|
||||
if not filters:
|
||||
filters = {}
|
||||
@@ -845,7 +862,7 @@ def update_course_statistics():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_announcements(batch):
|
||||
return frappe.get_all(
|
||||
communications = frappe.get_all(
|
||||
"Communication",
|
||||
filters={
|
||||
"reference_doctype": "LMS Batch",
|
||||
@@ -863,6 +880,13 @@ def get_announcements(batch):
|
||||
order_by="communication_date desc",
|
||||
)
|
||||
|
||||
for communication in communications:
|
||||
communication.image = frappe.get_cached_value(
|
||||
"User", communication.sender, "user_image"
|
||||
)
|
||||
|
||||
return communications
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_course(course):
|
||||
@@ -1228,13 +1252,54 @@ def is_guest_allowed():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_announcement(students, cc, subject, content):
|
||||
for student in students:
|
||||
frappe.sendmail(
|
||||
recipients=student,
|
||||
cc=cc,
|
||||
subject=subject,
|
||||
message=content,
|
||||
header=[subject, "green"],
|
||||
retry=3,
|
||||
def cancel_evaluation(evaluation):
|
||||
evaluation = frappe._dict(evaluation)
|
||||
|
||||
if evaluation.member != frappe.session.user:
|
||||
return
|
||||
|
||||
frappe.db.set_value("LMS Certificate Request", evaluation.name, "status", "Cancelled")
|
||||
events = frappe.get_all(
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-04-15 18:45:08.614466",
|
||||
"modified": "2025-02-24 12:17:08.436659",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Evaluator",
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
"column_break_3",
|
||||
"member",
|
||||
"member_name",
|
||||
"evaluator",
|
||||
"section_break_dlzh",
|
||||
"assignment_attachment",
|
||||
"answer",
|
||||
"section_break_ydgh",
|
||||
"column_break_oqqy",
|
||||
"evaluator",
|
||||
"status",
|
||||
"comments",
|
||||
"section_break_rqal",
|
||||
@@ -80,7 +81,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Comments"
|
||||
},
|
||||
{
|
||||
@@ -139,12 +140,16 @@
|
||||
{
|
||||
"fieldname": "column_break_oqqy",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ydgh",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-12-24 21:22:35.212732",
|
||||
"modified": "2025-02-17 18:40:53.374932",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment Submission",
|
||||
|
||||
@@ -8,25 +8,31 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"published",
|
||||
"section_break_earo",
|
||||
"title",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"allow_self_enrollment",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"timezone",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
"column_break_flwy",
|
||||
"seat_count",
|
||||
"evaluation_end_date",
|
||||
"section_break_cssv",
|
||||
"published",
|
||||
"column_break_wfkz",
|
||||
"allow_self_enrollment",
|
||||
"column_break_vnrp",
|
||||
"certification",
|
||||
"section_break_6",
|
||||
"description",
|
||||
"column_break_hlqw",
|
||||
"instructors",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
"confirmation_email_template",
|
||||
"column_break_flwy",
|
||||
"seat_count",
|
||||
"evaluation_end_date",
|
||||
"meta_image",
|
||||
"section_break_khcn",
|
||||
"batch_details",
|
||||
@@ -206,6 +212,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
@@ -318,6 +325,35 @@
|
||||
{
|
||||
"fieldname": "section_break_khcn",
|
||||
"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,
|
||||
@@ -335,7 +371,7 @@
|
||||
"link_fieldname": "batch_name"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-12 11:59:35.312487",
|
||||
"modified": "2025-02-18 15:43:18.512504",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
|
||||
@@ -8,9 +8,8 @@ import json
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
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 (
|
||||
get_lessons,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_quiz_details,
|
||||
@@ -176,6 +175,10 @@ def create_live_class(
|
||||
class_details = frappe.get_doc(payload)
|
||||
class_details.save()
|
||||
return class_details
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Error creating live class. Please try again. {0}").format(response.text)
|
||||
)
|
||||
|
||||
|
||||
def authenticate():
|
||||
@@ -254,17 +257,6 @@ def create_batch(
|
||||
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()
|
||||
def add_course(course, parent, name=None, evaluator=None):
|
||||
frappe.only_for("Moderator")
|
||||
@@ -405,3 +397,40 @@ def is_milestone_complete(idx, batch):
|
||||
return False
|
||||
|
||||
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):
|
||||
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(
|
||||
"LMS Batch",
|
||||
doc.batch,
|
||||
["name", "title", "start_date", "start_time", "medium"],
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"start_date",
|
||||
"start_time",
|
||||
"medium",
|
||||
"confirmation_email_template",
|
||||
],
|
||||
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 = {
|
||||
"title": batch.title,
|
||||
"student_name": doc.member_name,
|
||||
@@ -107,6 +116,6 @@ def send_mail(doc):
|
||||
template=template if not custom_template else None,
|
||||
content=content if custom_template else None,
|
||||
args=args,
|
||||
header=[subject, "green"],
|
||||
header=[_(batch.title), "green"],
|
||||
retry=3,
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-17 11:57:02.859109",
|
||||
"modified": "2025-02-19 16:26:05.902473",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
@@ -156,6 +156,7 @@
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
|
||||
@@ -122,11 +122,6 @@ def create_certificate(course):
|
||||
)
|
||||
|
||||
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(
|
||||
"Property Setter",
|
||||
{
|
||||
@@ -148,7 +143,6 @@ def create_certificate(course):
|
||||
"member": frappe.session.user,
|
||||
"course": course,
|
||||
"issue_date": nowdate(),
|
||||
"expiry_date": expiry_date,
|
||||
"template": default_certificate_template,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@ class TestLMSCertificate(unittest.TestCase):
|
||||
"Test Certificate",
|
||||
{
|
||||
"enable_certification": 1,
|
||||
"expiry": 2,
|
||||
},
|
||||
)
|
||||
certificate = create_certificate(course.name)
|
||||
@@ -24,7 +23,6 @@ class TestLMSCertificate(unittest.TestCase):
|
||||
self.assertEqual(certificate.member, "Administrator")
|
||||
self.assertEqual(certificate.course, course.name)
|
||||
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 Course", course.name)
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"google_meet_link",
|
||||
"column_break_ddyh",
|
||||
"start_time",
|
||||
"end_time"
|
||||
"end_time",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -144,11 +145,19 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-09-11 11:19:44.669132",
|
||||
"modified": "2025-02-19 17:20:02.526294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
@@ -204,6 +213,19 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"states": [
|
||||
{
|
||||
"color": "Blue",
|
||||
"title": "Upcoming"
|
||||
},
|
||||
{
|
||||
"color": "Green",
|
||||
"title": "Completed"
|
||||
},
|
||||
{
|
||||
"color": "Red",
|
||||
"title": "Cancelled"
|
||||
}
|
||||
],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
@@ -40,15 +40,13 @@
|
||||
"pricing_tab",
|
||||
"pricing_section",
|
||||
"paid_course",
|
||||
"enable_certification",
|
||||
"paid_certificate",
|
||||
"evaluator",
|
||||
"column_break_acoj",
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"certification_tab",
|
||||
"certification_section",
|
||||
"enable_certification",
|
||||
"column_break_rxww",
|
||||
"expiry",
|
||||
"tab_4_tab",
|
||||
"statistics_section",
|
||||
"enrollments",
|
||||
@@ -134,22 +132,11 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Course Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "certification_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_certification",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Certification"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "enable_certification",
|
||||
"fieldname": "expiry",
|
||||
"fieldtype": "Int",
|
||||
"label": "Certification Expires After (Years)"
|
||||
"label": "Completion Certificate"
|
||||
},
|
||||
{
|
||||
"fieldname": "related_courses",
|
||||
@@ -181,7 +168,6 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_course",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
@@ -195,22 +181,16 @@
|
||||
"label": "Paid Course"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_course",
|
||||
"fieldname": "course_price",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Course Price",
|
||||
"label": "Amount",
|
||||
"mandatory_depends_on": "paid_course"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rxww",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_acoj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_course",
|
||||
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
||||
"fieldname": "amount_usd",
|
||||
"fieldtype": "Currency",
|
||||
@@ -238,12 +218,7 @@
|
||||
{
|
||||
"fieldname": "pricing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Pricing"
|
||||
},
|
||||
{
|
||||
"fieldname": "certification_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Certification"
|
||||
"label": "Pricing and Certification"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_htgn",
|
||||
@@ -284,6 +259,19 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Rating",
|
||||
"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",
|
||||
@@ -310,7 +298,7 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-10-30 23:08:31.842860",
|
||||
"modified": "2025-02-24 11:50:58.325804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
|
||||
@@ -5,9 +5,8 @@ import json
|
||||
import random
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, today
|
||||
from frappe.utils.telemetry import capture
|
||||
from lms.lms.utils import get_chapters, can_create_courses
|
||||
from frappe.utils import today, cint
|
||||
from lms.lms.utils import get_chapters
|
||||
from ...utils import generate_slug, validate_image, update_payment_record
|
||||
from frappe import _
|
||||
|
||||
@@ -19,6 +18,7 @@ class LMSCourse(Document):
|
||||
self.validate_video_link()
|
||||
self.validate_status()
|
||||
self.validate_payments_app()
|
||||
self.validate_certification()
|
||||
self.validate_amount_and_currency()
|
||||
self.image = validate_image(self.image)
|
||||
|
||||
@@ -52,10 +52,22 @@ class LMSCourse(Document):
|
||||
if "payments" not in installed_apps:
|
||||
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):
|
||||
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."))
|
||||
|
||||
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):
|
||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||
self.send_email_to_interested_users()
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"member",
|
||||
"member_name",
|
||||
"member_username",
|
||||
"certification_section",
|
||||
"purchased_certificate",
|
||||
"certificate",
|
||||
"section_break_8",
|
||||
"cohort",
|
||||
"subgroup",
|
||||
@@ -123,11 +126,28 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "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,
|
||||
"links": [],
|
||||
"modified": "2024-10-30 12:44:16.103598",
|
||||
"modified": "2025-02-21 17:11:37.986157",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
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):
|
||||
@@ -56,8 +57,48 @@ class LMSLiveClass(Document):
|
||||
{
|
||||
"sync_with_google_calendar": 1,
|
||||
"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()
|
||||
|
||||
|
||||
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",
|
||||
"payment_received",
|
||||
"payment_for_certificate",
|
||||
"payment_details_section",
|
||||
"currency",
|
||||
"amount",
|
||||
@@ -136,11 +137,26 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Source",
|
||||
"options": "LMS Source"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "payment_for_certificate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Payment for Certificate"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-11 14:48:27.801895",
|
||||
"links": [
|
||||
{
|
||||
"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",
|
||||
"module": "LMS",
|
||||
"name": "LMS Payment",
|
||||
|
||||
@@ -10,12 +10,29 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
|
||||
|
||||
class LMSQuizSubmission(Document):
|
||||
def validate(self):
|
||||
self.validate_if_max_attempts_exceeded()
|
||||
self.validate_marks()
|
||||
self.set_percentage()
|
||||
|
||||
def on_update(self):
|
||||
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):
|
||||
self.score = 0
|
||||
for row in self.result:
|
||||
@@ -52,3 +69,7 @@ class LMSQuizSubmission(Document):
|
||||
)
|
||||
|
||||
make_notification_logs(notification, [self.member])
|
||||
|
||||
|
||||
class MaximumAttemptsExceededError(frappe.DuplicateEntryError):
|
||||
pass
|
||||
|
||||
@@ -18,19 +18,26 @@ def validate_currency(payment_gateway, currency):
|
||||
|
||||
|
||||
@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()
|
||||
address = frappe._dict(address)
|
||||
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)
|
||||
|
||||
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 = {
|
||||
"amount": total_amount,
|
||||
"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
|
||||
|
||||
|
||||
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_name = save_address(address)
|
||||
|
||||
@@ -71,6 +86,7 @@ def record_payment(address, doctype, docname, amount, currency, amount_with_gst=
|
||||
"source": address.source,
|
||||
"payment_for_document_type": doctype,
|
||||
"payment_for_document": docname,
|
||||
"payment_for_certificate": payment_for_certificate,
|
||||
}
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
def get_membership(course, member=None, batch=None):
|
||||
def get_membership(course, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
filters = {"member": member, "course": course}
|
||||
if batch:
|
||||
filters["batch_old"] = batch
|
||||
|
||||
is_member = frappe.db.exists("LMS Enrollment", filters)
|
||||
if is_member:
|
||||
if frappe.db.exists("LMS Enrollment", filters):
|
||||
membership = frappe.db.get_value(
|
||||
"LMS Enrollment",
|
||||
filters,
|
||||
["name", "batch_old", "current_lesson", "member_type", "progress", "member"],
|
||||
[
|
||||
"name",
|
||||
"current_lesson",
|
||||
"progress",
|
||||
"member",
|
||||
"purchased_certificate",
|
||||
"certificate",
|
||||
],
|
||||
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 False
|
||||
@@ -856,26 +855,44 @@ def is_onboarding_complete():
|
||||
}
|
||||
|
||||
|
||||
def get_evaluator(course, batch):
|
||||
def get_evaluator(course, batch=None):
|
||||
evaluator = None
|
||||
evaluator = frappe.db.get_value(
|
||||
"Batch Course",
|
||||
{"parent": batch, "course": course},
|
||||
"evaluator",
|
||||
)
|
||||
if batch:
|
||||
evaluator = frappe.db.get_value(
|
||||
"Batch Course",
|
||||
{"parent": batch, "course": course},
|
||||
"evaluator",
|
||||
)
|
||||
else:
|
||||
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
||||
return evaluator
|
||||
|
||||
|
||||
@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(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
"member": student,
|
||||
"course": ["in", courses],
|
||||
"date": [">=", frappe.utils.nowdate()],
|
||||
},
|
||||
["date", "start_time", "course", "evaluator", "google_meet_link"],
|
||||
filters,
|
||||
[
|
||||
"name",
|
||||
"date",
|
||||
"start_time",
|
||||
"course",
|
||||
"evaluator",
|
||||
"google_meet_link",
|
||||
"member",
|
||||
"member_name",
|
||||
],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
@@ -999,6 +1016,7 @@ def get_course_details(course):
|
||||
"category",
|
||||
"status",
|
||||
"paid_course",
|
||||
"paid_certificate",
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
@@ -1013,7 +1031,7 @@ def get_course_details(course):
|
||||
|
||||
course_details.instructors = get_instructors(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, None, course_details.amount_usd
|
||||
)"""
|
||||
@@ -1126,14 +1144,21 @@ def get_lesson(course, chapter, lesson):
|
||||
return {}
|
||||
|
||||
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 (
|
||||
not lesson_details.include_in_preview
|
||||
and not membership
|
||||
and not has_course_moderator_role()
|
||||
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(
|
||||
"Course Lesson",
|
||||
@@ -1168,7 +1193,8 @@ def get_lesson(course, chapter, lesson):
|
||||
lesson_details.prev = neighbours["prev"]
|
||||
lesson_details.membership = membership
|
||||
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
|
||||
|
||||
|
||||
@@ -1194,6 +1220,16 @@ def get_neighbour_lesson(course, chapter, lesson):
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
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(
|
||||
"LMS Batch",
|
||||
batch,
|
||||
@@ -1215,6 +1251,7 @@ def get_batch_details(batch):
|
||||
"paid_batch",
|
||||
"evaluation_end_date",
|
||||
"allow_self_enrollment",
|
||||
"certification",
|
||||
"timezone",
|
||||
"category",
|
||||
],
|
||||
@@ -1222,13 +1259,12 @@ def get_batch_details(batch):
|
||||
)
|
||||
|
||||
batch_details.instructors = get_instructors(batch)
|
||||
batch_details.accept_enrollments = batch_details.start_date > getdate()
|
||||
|
||||
batch_details.courses = frappe.get_all(
|
||||
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
|
||||
)
|
||||
batch_details.students = frappe.get_all(
|
||||
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
|
||||
)
|
||||
batch_details.students = batch_students
|
||||
|
||||
if batch_details.paid_batch and batch_details.start_date >= getdate():
|
||||
batch_details.amount, batch_details.currency = check_multicurrency(
|
||||
@@ -1450,7 +1486,7 @@ def get_batch_students(batch):
|
||||
)
|
||||
detail.assessments[title] = assessment_info
|
||||
|
||||
if assessment_info.result == "Passed":
|
||||
if assessment_info.result == "Pass":
|
||||
assessments_completed += 1
|
||||
|
||||
detail.courses_completed = courses_completed
|
||||
@@ -1493,20 +1529,26 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
|
||||
|
||||
attempt = frappe.db.exists(doctype, filters)
|
||||
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":
|
||||
result = "Failed"
|
||||
passing_percentage = frappe.db.get_value(
|
||||
"LMS Quiz", assessment, "passing_percentage"
|
||||
)
|
||||
if attempt_details >= passing_percentage:
|
||||
result = "Passed"
|
||||
if attempt_details.percentage >= passing_percentage:
|
||||
result = "Pass"
|
||||
else:
|
||||
result = attempt_details
|
||||
result = attempt_details.status
|
||||
return frappe._dict(
|
||||
{
|
||||
"status": attempt_details,
|
||||
"status": attempt_details.percentage
|
||||
if assessment_type == "LMS Quiz"
|
||||
else attempt_details.status,
|
||||
"result": result,
|
||||
"assessment": assessment,
|
||||
"type": assessment_type,
|
||||
"submission": attempt_details.name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -1586,11 +1628,19 @@ def get_order_summary(doctype, docname, country=None):
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
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,
|
||||
)
|
||||
|
||||
if not details.paid_course:
|
||||
if not details.paid_course and not details.paid_certificate:
|
||||
raise frappe.throw(_("This course is free."))
|
||||
|
||||
else:
|
||||
@@ -1704,9 +1754,14 @@ def update_payment_record(doctype, docname):
|
||||
"order_id": data.get("order_id"),
|
||||
},
|
||||
)
|
||||
payment_for_certificate = frappe.db.get_value(
|
||||
"LMS Payment", data.payment, "payment_for_certificate"
|
||||
)
|
||||
|
||||
try:
|
||||
if doctype == "LMS Course":
|
||||
if payment_for_certificate:
|
||||
update_certificate_purchase(docname)
|
||||
elif doctype == "LMS Course":
|
||||
enroll_in_course(data.payment, docname)
|
||||
else:
|
||||
enroll_in_batch(docname, data.payment)
|
||||
@@ -1766,6 +1821,15 @@ def enroll_in_batch(batch, payment_name=None):
|
||||
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()
|
||||
def get_programs():
|
||||
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
2025
lms/locale/bs.po
2025
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
721
lms/locale/fa.po
721
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.allow_guest_access #05-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!") }}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
<b>
|
||||
{{ title }}
|
||||
</b>
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
|
||||
@@ -27,7 +21,7 @@
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Visit the following link to view your ") }}
|
||||
<a href="/batches/{{ name }}">{{ _("Batch Details") }}</a>
|
||||
<a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
|
||||
</p>
|
||||
<p>
|
||||
{{ _("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>
|
||||
<p>{{ _('Hi') }} {{ billing_name }},</p>
|
||||
<br>
|
||||
<p>{{ _('We noticed that you started enrolling in the') }} {{ type }} {{ title }} {{ _('but didn’t complete your payment') }}.</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("We have a limited number of seats, and they won't be available for long!")}}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Don’t miss this opportunity to enhance your skills. Click below to complete your enrollment") }}:
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
<a href="{{ link }}">👉 Complete Your Enrollment</a>
|
||||
<a href="{{ link }}">👉 {{ _("Complete Your Enrollment") }}</a>
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("If you have any questions or need assistance, feel free to reach out to our support team.") }}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Looking forward to seeing you enrolled!") }}
|
||||
</p>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import frappe
|
||||
from frappe.utils.telemetry import capture
|
||||
from frappe import _
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
from frappe import _
|
||||
from frappe.utils.telemetry import capture
|
||||
from frappe.utils import cint
|
||||
|
||||
no_cache = 1
|
||||
|
||||
@@ -17,6 +18,7 @@ def get_context():
|
||||
csrf_token = frappe.sessions.get_csrf_token()
|
||||
frappe.db.commit() # nosemgrep
|
||||
context.csrf_token = csrf_token
|
||||
context.setup_complete = cint(frappe.get_system_settings("setup_complete"))
|
||||
capture("active_site", "lms")
|
||||
context.favicon = frappe.db.get_single_value("Website Settings", "favicon")
|
||||
return context
|
||||
|
||||
Reference in New Issue
Block a user