Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
795d95b482 | ||
|
|
5b5b95c85c | ||
|
|
8490b07c90 | ||
|
|
dee2c51c60 | ||
|
|
4149fa6ce4 | ||
|
|
7a69611f09 | ||
|
|
6692252df9 | ||
|
|
486ce1bdb0 | ||
|
|
cceff77bc2 | ||
|
|
22a9169f87 | ||
|
|
47a30763a0 | ||
|
|
73379a1bd8 | ||
|
|
7cc46629b4 | ||
|
|
67304245ba | ||
|
|
8edd3a1a34 | ||
|
|
e4bc7c8d78 | ||
|
|
a8af78d400 | ||
|
|
0afe3de818 | ||
|
|
3c81aadec6 | ||
|
|
1dfcb035da | ||
|
|
77b24882a9 | ||
|
|
1fd0673257 | ||
|
|
dbda76e0ce | ||
|
|
a9d22521ce | ||
|
|
6da1d9629f | ||
|
|
37b61a7087 | ||
|
|
9b484e6ee9 | ||
|
|
5ef67ef21c | ||
|
|
f902166643 | ||
|
|
8f91466b3d | ||
|
|
fa1621c3d1 | ||
|
|
2acd45feae | ||
|
|
f19e974b9d | ||
|
|
01598ac002 | ||
|
|
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'
|
node-version: '18'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: setup cache for bench
|
- name: setup cache for bench
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/bench-cache
|
path: ~/bench-cache
|
||||||
key: ${{ runner.os }}
|
key: ${{ runner.os }}
|
||||||
|
|||||||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts
|
echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Cache pip
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://lms1:8000",
|
baseUrl: "http://testui:8000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe("Course Creation", () => {
|
|||||||
cy.visit("/lms/courses");
|
cy.visit("/lms/courses");
|
||||||
|
|
||||||
// Create a course
|
// Create a course
|
||||||
cy.get("header").children().last().children().last().click();
|
cy.get("button").contains("New").click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.url().should("include", "/courses/new/edit");
|
cy.url().should("include", "/courses/new/edit");
|
||||||
|
|
||||||
@@ -84,9 +84,8 @@ describe("Course Creation", () => {
|
|||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
|
|
||||||
cy.get("label").contains("Title").type("Test Lesson");
|
cy.get("label").contains("Title").type("Test Lesson");
|
||||||
|
|
||||||
cy.get("#content .ce-block").type(
|
cy.get("#content .ce-block").type(
|
||||||
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
"{enter}This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
||||||
);
|
);
|
||||||
cy.button("Save").click();
|
cy.button("Save").click();
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Cypress.Commands.add("login", (email, password) => {
|
|||||||
url: "/api/method/login",
|
url: "/api/method/login",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { usr: email, pwd: password },
|
body: { usr: email, pwd: password },
|
||||||
|
timeout: 60000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.csrf_token = '{{ csrf_token }}'
|
window.csrf_token = '{{ csrf_token }}'
|
||||||
|
window.setup_complete = '{{ setup_complete }}'
|
||||||
document.getElementById('seo-content').style.display = 'none';
|
document.getElementById('seo-content').style.display = 'none';
|
||||||
</script>
|
</script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|||||||
@@ -19,13 +19,14 @@
|
|||||||
"@editorjs/paragraph": "^2.11.3",
|
"@editorjs/paragraph": "^2.11.3",
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
"@editorjs/simple-image": "^1.6.0",
|
||||||
"@editorjs/table": "^2.4.2",
|
"@editorjs/table": "^2.4.2",
|
||||||
|
"@vueuse/router": "^12.7.0",
|
||||||
"ace-builds": "^1.36.2",
|
"ace-builds": "^1.36.2",
|
||||||
"apexcharts": "^4.3.0",
|
"apexcharts": "^4.3.0",
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"codemirror-editor-vue3": "^2.8.0",
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.109",
|
"frappe-ui": "^0.1.112",
|
||||||
"lucide-vue-next": "^0.383.0",
|
"lucide-vue-next": "^0.383.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
|
|||||||
@@ -62,25 +62,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SidebarLink
|
<div>
|
||||||
:link="{
|
<TrialBanner
|
||||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
v-if="
|
||||||
}"
|
userResource.data?.is_system_manager && userResource.data?.is_fc_site
|
||||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
"
|
||||||
@click="toggleSidebar()"
|
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
class="m-2"
|
/>
|
||||||
>
|
<SidebarLink
|
||||||
<template #icon>
|
:link="{
|
||||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||||
<CollapseSidebar
|
}"
|
||||||
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
|
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
:class="{
|
@click="toggleSidebar()"
|
||||||
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
class="m-2"
|
||||||
}"
|
>
|
||||||
/>
|
<template #icon>
|
||||||
</span>
|
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||||
</template>
|
<CollapseSidebar
|
||||||
</SidebarLink>
|
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
|
||||||
|
:class="{
|
||||||
|
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</SidebarLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PageModal
|
<PageModal
|
||||||
v-model="showPageModal"
|
v-model="showPageModal"
|
||||||
@@ -101,7 +109,7 @@ import { sessionStore } from '@/stores/session'
|
|||||||
import { useSidebar } from '@/stores/sidebar'
|
import { useSidebar } from '@/stores/sidebar'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import { ChevronRight, Plus } from 'lucide-vue-next'
|
import { ChevronRight, Plus } from 'lucide-vue-next'
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { Button, createResource, TrialBanner } from 'frappe-ui'
|
||||||
import PageModal from '@/components/Modals/PageModal.vue'
|
import PageModal from '@/components/Modals/PageModal.vue'
|
||||||
|
|
||||||
const { user, sidebarSettings } = sessionStore()
|
const { user, sidebarSettings } = sessionStore()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="assignment.data"
|
v-if="assignment.data"
|
||||||
class="grid grid-cols-[68%,32%] h-full"
|
class="grid grid-cols-[65%,35%] h-full"
|
||||||
:class="{ 'border rounded-lg': !showTitle }"
|
:class="{ 'border rounded-lg': !showTitle }"
|
||||||
>
|
>
|
||||||
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]">
|
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]">
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex items-center text-ink-gray-7">
|
<div class="flex text-ink-gray-7">
|
||||||
<div class="border rounded-md p-2 mr-2">
|
<div class="border self-start rounded-md p-2 mr-2">
|
||||||
<FileText class="h-5 w-5 stroke-1.5" />
|
<FileText class="h-5 w-5 stroke-1.5" />
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex flex-col cursor-pointer !no-underline"
|
class="flex flex-col cursor-pointer !no-underline"
|
||||||
>
|
>
|
||||||
<span>
|
<span class="text-sm leading-5">
|
||||||
{{ submissionFile.file_name }}
|
{{ submissionFile.file_name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-ink-gray-5 mt-1">
|
<span class="text-sm text-ink-gray-5 mt-1">
|
||||||
@@ -155,12 +155,23 @@
|
|||||||
type="select"
|
type="select"
|
||||||
:options="submissionStatusOptions"
|
:options="submissionStatusOptions"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<div>
|
||||||
v-if="submissionResource.doc"
|
<div class="text-sm text-ink-gray-5 mb-1">
|
||||||
v-model="submissionResource.doc.comments"
|
{{ __('Comments') }}
|
||||||
:label="__('Comments')"
|
</div>
|
||||||
type="textarea"
|
<TextEditor
|
||||||
/>
|
:content="comments"
|
||||||
|
@change="
|
||||||
|
(val) => {
|
||||||
|
comments = val
|
||||||
|
isDirty = true
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +195,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const submissionFile = ref(null)
|
const submissionFile = ref(null)
|
||||||
const answer = ref(null)
|
const answer = ref(null)
|
||||||
|
const comments = ref(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
|
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
|
||||||
@@ -281,6 +293,9 @@ watch(submissionResource, () => {
|
|||||||
if (submissionResource.doc.answer) {
|
if (submissionResource.doc.answer) {
|
||||||
answer.value = submissionResource.doc.answer
|
answer.value = submissionResource.doc.answer
|
||||||
}
|
}
|
||||||
|
if (submissionResource.doc.comments) {
|
||||||
|
comments.value = submissionResource.doc.comments
|
||||||
|
}
|
||||||
if (submissionResource.isDirty) {
|
if (submissionResource.isDirty) {
|
||||||
isDirty.value = true
|
isDirty.value = true
|
||||||
} else if (showUploader() && !submissionFile.value) {
|
} else if (showUploader() && !submissionFile.value) {
|
||||||
@@ -305,11 +320,14 @@ const submitAssignment = () => {
|
|||||||
submissionResource.doc && submissionResource.doc.owner != user.data?.name
|
submissionResource.doc && submissionResource.doc.owner != user.data?.name
|
||||||
? user.data?.name
|
? user.data?.name
|
||||||
: null
|
: null
|
||||||
|
|
||||||
submissionResource.setValue.submit(
|
submissionResource.setValue.submit(
|
||||||
{
|
{
|
||||||
...submissionResource.doc,
|
...submissionResource.doc,
|
||||||
assignment_attachment: submissionFile.value?.file_url,
|
assignment_attachment: submissionFile.value?.file_url,
|
||||||
evaluator: evaluator,
|
evaluator: evaluator,
|
||||||
|
comments: comments.value,
|
||||||
|
answer: answer.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div v-if="column.key == 'member_name'">
|
<div v-if="column.key == 'member_name'">
|
||||||
<Avatar
|
<Avatar
|
||||||
class="flex items-center"
|
class="flex"
|
||||||
:image="row['member_image']"
|
:image="row['member_image']"
|
||||||
:label="item"
|
:label="item"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -240,6 +240,6 @@ const feedbackColumns = computed(() => {
|
|||||||
<style>
|
<style>
|
||||||
.feedback-list > button > div {
|
.feedback-list > button > div {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
padding: 0.25rem 0;
|
padding: 0.15rem 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
||||||
<div
|
<div
|
||||||
v-if="batch.data.seat_count && seats_left > 0"
|
v-if="batch.data.seat_count && seats_left > 0"
|
||||||
class="text-xs bg-green-200 text-green-800 float-right px-2 py-0.5 rounded-md"
|
class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md"
|
||||||
>
|
>
|
||||||
{{ seats_left }}
|
{{ seats_left }}
|
||||||
<span v-if="seats_left > 1">
|
<span v-if="seats_left > 1">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="batch.data.seat_count && seats_left <= 0"
|
v-else-if="batch.data.seat_count && seats_left <= 0"
|
||||||
class="text-xs bg-red-200 text-red-900 float-right px-2 py-0.5 rounded-md"
|
class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
|
||||||
>
|
>
|
||||||
{{ __('Sold Out') }}
|
{{ __('Sold Out') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +69,11 @@
|
|||||||
name: batch.data.name,
|
name: batch.data.name,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
v-else-if="batch.data.paid_batch && batch.data.seats_left"
|
v-else-if="
|
||||||
|
batch.data.paid_batch &&
|
||||||
|
batch.data.seats_left > 0 &&
|
||||||
|
batch.data.accept_enrollments
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
|
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
|
||||||
<span>
|
<span>
|
||||||
@@ -80,7 +84,11 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
v-else-if="batch.data.allow_self_enrollment && batch.data.seats_left"
|
v-else-if="
|
||||||
|
batch.data.allow_self_enrollment &&
|
||||||
|
batch.data.seats_left &&
|
||||||
|
batch.data.accept_enrollments
|
||||||
|
"
|
||||||
@click="enrollInBatch()"
|
@click="enrollInBatch()"
|
||||||
>
|
>
|
||||||
{{ __('Enroll Now') }}
|
{{ __('Enroll Now') }}
|
||||||
@@ -112,6 +120,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="chartData"
|
:series="chartData"
|
||||||
type="bar"
|
type="bar"
|
||||||
height="200"
|
:height="chartData[0].data.length * 30 + 100"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4"
|
class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4"
|
||||||
@@ -357,7 +357,7 @@ const getChartData = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Object.keys(student.assessments).forEach((assessment) => {
|
Object.keys(student.assessments).forEach((assessment) => {
|
||||||
if (student.assessments[assessment].result === 'Passed') {
|
if (student.assessments[assessment].result === 'Pass') {
|
||||||
categories[assessment].value += 1
|
categories[assessment].value += 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
67
frontend/src/components/CertificationLinks.vue
Normal file
67
frontend/src/components/CertificationLinks.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
certification.data &&
|
||||||
|
certification.data.membership &&
|
||||||
|
certification.data.paid_certificate &&
|
||||||
|
user.data?.is_student
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="!certification.data.membership.purchased_certificate"
|
||||||
|
:to="{
|
||||||
|
name: 'Billing',
|
||||||
|
params: {
|
||||||
|
type: 'certificate',
|
||||||
|
name: courseName,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<GraduationCap class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Get Certified') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-else-if="!certification.data.membership.certficate"
|
||||||
|
:to="{
|
||||||
|
name: 'CourseCertification',
|
||||||
|
params: {
|
||||||
|
courseName: courseName,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<GraduationCap class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Get Certified') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Button, createResource } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { GraduationCap } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const certification = createResource({
|
||||||
|
url: 'lms.lms.api.get_certification_details',
|
||||||
|
params: {
|
||||||
|
course: props.courseName,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: ['certificationData', user.data?.name],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -130,7 +130,7 @@ import {
|
|||||||
ComboboxOptions,
|
ComboboxOptions,
|
||||||
ComboboxOption,
|
ComboboxOption,
|
||||||
} from '@headlessui/vue'
|
} from '@headlessui/vue'
|
||||||
import { Popover, Button } from 'frappe-ui'
|
import { Popover } from 'frappe-ui'
|
||||||
import { ChevronDown, X } from 'lucide-vue-next'
|
import { ChevronDown, X } from 'lucide-vue-next'
|
||||||
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
|
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
{{ __('Featured') }}
|
{{ __('Featured') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div
|
<div
|
||||||
v-for="tag in course.tags"
|
v-if="course.tags"
|
||||||
|
v-for="tag in course.tags?.split(', ')"
|
||||||
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md"
|
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
@@ -100,9 +101,15 @@
|
|||||||
<CourseInstructors :instructors="course.instructors" />
|
<CourseInstructors :instructors="course.instructors" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="font-semibold">
|
<div v-if="course.paid_course" class="font-semibold">
|
||||||
{{ course.price }}
|
{{ course.price }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="course.paid_certificate || course.enable_certification"
|
||||||
|
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
|
||||||
|
>
|
||||||
|
{{ __('Certification') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,30 +6,32 @@
|
|||||||
class="rounded-t-md min-h-56 w-full"
|
class="rounded-t-md min-h-56 w-full"
|
||||||
/>
|
/>
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div v-if="course.data.price" class="text-2xl font-semibold mb-3">
|
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
|
||||||
{{ course.data.price }}
|
{{ course.data.price }}
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<div v-if="course.data.membership" class="space-y-2">
|
||||||
v-if="course.data.membership"
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Lesson',
|
name: 'Lesson',
|
||||||
params: {
|
params: {
|
||||||
courseName: course.name,
|
courseName: course.name,
|
||||||
chapterNumber: course.data.current_lesson
|
chapterNumber: course.data.current_lesson
|
||||||
? course.data.current_lesson.split('-')[0]
|
? course.data.current_lesson.split('-')[0]
|
||||||
: 1,
|
: 1,
|
||||||
lessonNumber: course.data.current_lesson
|
lessonNumber: course.data.current_lesson
|
||||||
? course.data.current_lesson.split('-')[1]
|
? course.data.current_lesson.split('-')[1]
|
||||||
: 1,
|
: 1,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button variant="solid" size="md" class="w-full">
|
<Button variant="solid" size="md" class="w-full">
|
||||||
<span>
|
<span>
|
||||||
{{ __('Continue Learning') }}
|
{{ __('Continue Learning') }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<CertificationLinks :courseName="course.data.name" />
|
||||||
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-else-if="course.data.paid_course"
|
v-else-if="course.data.paid_course"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -113,17 +115,36 @@
|
|||||||
{{ course.data.rating }} {{ __('Rating') }}
|
{{ course.data.rating }} {{ __('Rating') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="course.data.enable_certification"
|
||||||
|
class="flex items-center font-semibold text-ink-gray-9"
|
||||||
|
>
|
||||||
|
<GraduationCap class="h-4 w-4 stroke-2" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ __('Certificate of Completion') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="course.data.paid_certificate"
|
||||||
|
class="flex items-center font-semibold text-ink-gray-9"
|
||||||
|
>
|
||||||
|
<GraduationCap class="h-4 w-4 stroke-2" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ __('Paid Certificate after Evaluation') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||||
import { showToast, formatAmount } from '@/utils/'
|
import { showToast, formatAmount } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<div class="flex mt-2">
|
<div class="flex mt-2">
|
||||||
<Star
|
<Star
|
||||||
v-for="index in 5"
|
v-for="index in 5"
|
||||||
class="h-5 w-5 text-ink-gray-2 rounded-sm mr-2"
|
class="h-5 w-5 text-ink-gray-1 rounded-sm mr-2"
|
||||||
:class="
|
:class="
|
||||||
index <= Math.ceil(review.rating)
|
index <= Math.ceil(review.rating)
|
||||||
? 'fill-orange-500'
|
? 'fill-orange-500'
|
||||||
|
|||||||
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>
|
<template>
|
||||||
<div class="flex space-x-4 border rounded-md p-2">
|
<div class="flex space-x-4 border rounded-md p-2">
|
||||||
<Avatar :image="job.company_logo" :label="job.job_title" size="2xl" />
|
<img :src="job.company_logo" class="size-10 rounded-full object-contain" />
|
||||||
<div class="flex flex-col space-y-2 flex-1">
|
<div class="flex flex-col space-y-2 flex-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold text-ink-gray-9">
|
<span class="font-semibold text-ink-gray-9">
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-model="member.first_name"
|
v-model="member.first_name"
|
||||||
:placeholder="__('First Name')"
|
:placeholder="__('First Name')"
|
||||||
type="test"
|
type="text"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<Button @click="addMember()" variant="subtle">
|
<Button @click="addMember()" variant="subtle">
|
||||||
|
|||||||
@@ -67,13 +67,16 @@ const announcement = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const announcementResource = createResource({
|
const announcementResource = createResource({
|
||||||
url: 'lms.lms.api.make_announcement',
|
url: 'frappe.core.doctype.communication.email.make',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
students: props.students,
|
recipients: props.students.join(', '),
|
||||||
cc: announcement.replyTo,
|
cc: announcement.replyTo,
|
||||||
subject: announcement.subject,
|
subject: announcement.subject,
|
||||||
content: announcement.announcement,
|
content: announcement.announcement,
|
||||||
|
doctype: 'LMS Batch',
|
||||||
|
name: props.batch,
|
||||||
|
send_email: 1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createResource } from 'frappe-ui'
|
import { Dialog, createResource } from 'frappe-ui'
|
||||||
import { ref, defineModel } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { showToast } from '@/utils'
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -32,25 +32,44 @@
|
|||||||
{{ __('Assessment') }}
|
{{ __('Assessment') }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ __('Progress') }}
|
{{ __('Percentage/Status') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<router-link
|
||||||
v-for="assessment in Object.keys(student.assessments)"
|
v-for="assessment in Object.keys(student.assessments)"
|
||||||
class="flex items-center text-ink-gray-7 font-medium"
|
class="flex items-center text-ink-gray-7 font-medium"
|
||||||
|
:to="{
|
||||||
|
name:
|
||||||
|
student.assessments[assessment].type == 'LMS Assignment'
|
||||||
|
? 'AssignmentSubmission'
|
||||||
|
: '',
|
||||||
|
params:
|
||||||
|
student.assessments[assessment].type == 'LMS Assignment'
|
||||||
|
? {
|
||||||
|
assignmentID:
|
||||||
|
student.assessments[assessment].assessment,
|
||||||
|
submissionName:
|
||||||
|
student.assessments[assessment].submission,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<span class="flex-1">
|
<span class="flex-1">
|
||||||
{{ assessment }}
|
{{ assessment }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="isAssignment(student.assessments[assessment])">
|
<span v-if="isAssignment(student.assessments[assessment].status)">
|
||||||
<Badge :theme="getStatusTheme(student.assessments[assessment])">
|
<Badge
|
||||||
{{ student.assessments[assessment] }}
|
:theme="
|
||||||
|
getStatusTheme(student.assessments[assessment].status)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ student.assessments[assessment].status }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ student.assessments[assessment] }}
|
{{ student.assessments[assessment].status }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Courses -->
|
<!-- Courses -->
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch } from 'vue'
|
import { reactive, watch } from 'vue'
|
||||||
import { showToast, getFileSize } from '@/utils/'
|
import { showToast, getFileSize } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
||||||
import { reactive, defineModel } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { showToast, singularize } from '@/utils'
|
import { showToast, singularize } from '@/utils'
|
||||||
|
|
||||||
const topics = defineModel('reloadTopics')
|
const topics = defineModel('reloadTopics')
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, watch, defineModel } from 'vue'
|
import { reactive, watch } from 'vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
import { getFileSize, showToast, escapeHTML } from '@/utils'
|
import { getFileSize, showToast, escapeHTML } from '@/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,15 @@
|
|||||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
{{ __('Date') }}
|
{{ __('Date') }}
|
||||||
</div>
|
</div>
|
||||||
<FormControl type="date" v-model="evaluation.date" />
|
<FormControl
|
||||||
|
type="date"
|
||||||
|
v-model="evaluation.date"
|
||||||
|
:min="
|
||||||
|
dayjs()
|
||||||
|
.add(dayjs.duration({ days: 1 }))
|
||||||
|
.format('YYYY-MM-DD')
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slots.data?.length">
|
<div v-if="slots.data?.length">
|
||||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
@@ -58,7 +66,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
|
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch, inject } from 'vue'
|
import { reactive, watch, inject } from 'vue'
|
||||||
import { createToast, formatTime } from '@/utils/'
|
import { createToast, formatTime } from '@/utils/'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -161,6 +169,11 @@ const getCourses = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (courses.length == 1) {
|
||||||
|
evaluation.course = courses[0].value
|
||||||
|
}
|
||||||
|
|
||||||
return courses
|
return courses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
|
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
|
||||||
import { FileText } from 'lucide-vue-next'
|
import { FileText } from 'lucide-vue-next'
|
||||||
import { ref, inject, defineModel } from 'vue'
|
import { ref, inject } from 'vue'
|
||||||
import { createToast, getFileSize } from '@/utils/'
|
import { createToast, getFileSize } from '@/utils/'
|
||||||
|
|
||||||
const resume = ref(null)
|
const resume = ref(null)
|
||||||
|
|||||||
@@ -39,13 +39,19 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<FormControl
|
|
||||||
v-model="liveClass.timezone"
|
<div class="space-y-1.5">
|
||||||
type="select"
|
<label class="block text-ink-gray-5 text-xs" for="batchTimezone">
|
||||||
:options="getTimezoneOptions()"
|
{{ __('Timezone') }}
|
||||||
:label="__('Timezone')"
|
<span class="text-ink-red-3">*</span>
|
||||||
:required="true"
|
</label>
|
||||||
/>
|
<Autocomplete
|
||||||
|
@update:modelValue="(opt) => (liveClass.timezone = opt.value)"
|
||||||
|
:modelValue="liveClass.timezone"
|
||||||
|
:options="getTimezoneOptions()"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -83,18 +89,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Input,
|
|
||||||
DatePicker,
|
|
||||||
Select,
|
|
||||||
Textarea,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
createResource,
|
createResource,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
Autocomplete,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, inject } from 'vue'
|
import { reactive, inject, onMounted } from 'vue'
|
||||||
import { getTimezones, createToast } from '@/utils/'
|
import { getTimezones, createToast, getUserTimezone } from '@/utils/'
|
||||||
import { Info } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const liveClasses = defineModel('reloadLiveClasses')
|
const liveClasses = defineModel('reloadLiveClasses')
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
@@ -120,6 +122,10 @@ let liveClass = reactive({
|
|||||||
host: user.data.name,
|
host: user.data.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
liveClass.timezone = getUserTimezone()
|
||||||
|
})
|
||||||
|
|
||||||
const getTimezoneOptions = () => {
|
const getTimezoneOptions = () => {
|
||||||
return getTimezones().map((timezone) => {
|
return getTimezones().map((timezone) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
import { Dialog, Textarea, createResource } from 'frappe-ui'
|
||||||
import { defineModel, reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import Rating from '@/components/Controls/Rating.vue'
|
import Rating from '@/components/Controls/Rating.vue'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast } from '@/utils/'
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="activeQuestion != questions.length"
|
v-else-if="activeQuestion != questions.length"
|
||||||
@click="nextQuetion()"
|
@click="nextQuestion()"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ __('Next') }}
|
{{ __('Next') }}
|
||||||
@@ -258,14 +258,22 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="quiz.data.show_submission_history && attempts?.data"
|
v-if="
|
||||||
|
quiz.data.show_submission_history &&
|
||||||
|
attempts?.data &&
|
||||||
|
attempts.data.length > 0
|
||||||
|
"
|
||||||
class="mt-10"
|
class="mt-10"
|
||||||
>
|
>
|
||||||
<ListView
|
<ListView
|
||||||
:columns="getSubmissionColumns()"
|
:columns="getSubmissionColumns()"
|
||||||
:rows="attempts?.data"
|
:rows="attempts?.data"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{ selectable: false, showTooltip: false }"
|
:options="{
|
||||||
|
selectable: false,
|
||||||
|
showTooltip: false,
|
||||||
|
emptyState: { title: __('No Quiz submissions found') },
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +290,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, watch, reactive, inject, computed } from 'vue'
|
import { ref, watch, reactive, inject, computed } from 'vue'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast, showToast } from '@/utils/'
|
||||||
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
||||||
import { timeAgo } from '@/utils'
|
import { timeAgo } from '@/utils'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -536,7 +544,7 @@ const addToLocalStorage = () => {
|
|||||||
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
|
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextQuetion = () => {
|
const nextQuestion = () => {
|
||||||
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
|
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
|
||||||
checkAnswer()
|
checkAnswer()
|
||||||
} else {
|
} else {
|
||||||
@@ -574,6 +582,16 @@ const createSubmission = () => {
|
|||||||
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
||||||
if (quiz.data.duration) clearInterval(timerInterval)
|
if (quiz.data.duration) clearInterval(timerInterval)
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
const errorTitle = err?.message || ''
|
||||||
|
if (errorTitle.includes('MaximumAttemptsExceededError')) {
|
||||||
|
const errorMessage = err.messages?.[0] || err
|
||||||
|
showToast(__('Error'), __(errorMessage), 'x')
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
<div class="text-lg font-semibold">
|
<div class="text-lg font-semibold">
|
||||||
{{ __('Upcoming Evaluations') }}
|
{{ __('Upcoming Evaluations') }}
|
||||||
</div>
|
</div>
|
||||||
<Button @click="openEvalModal">
|
<Button
|
||||||
|
v-if="
|
||||||
|
!upcoming_evals.data?.length ||
|
||||||
|
upcoming_evals.length == courses.length
|
||||||
|
"
|
||||||
|
@click="openEvalModal"
|
||||||
|
>
|
||||||
{{ __('Schedule Evaluation') }}
|
{{ __('Schedule Evaluation') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="upcoming_evals.data?.length">
|
<div v-if="upcoming_evals.data?.length">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div v-for="evl in upcoming_evals.data">
|
<div v-for="evl in upcoming_evals.data">
|
||||||
<div class="border rounded-md p-3">
|
<div class="border rounded-md p-3">
|
||||||
<div class="font-semibold mb-3">
|
<div class="font-semibold mb-3">
|
||||||
@@ -28,17 +34,39 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<UserCog2 class="w-4 h-4 stroke-1.5" />
|
<GraduationCap class="w-4 h-4 stroke-1.5" />
|
||||||
<span class="ml-2 font-medium">
|
<span class="ml-2">
|
||||||
{{ evl.evaluator_name }}
|
{{ evl.evaluator_name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between space-x-2 mt-4">
|
||||||
|
<Button
|
||||||
|
v-if="evl.google_meet_link"
|
||||||
|
@click="openEvalCall(evl)"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<HeadsetIcon class="w-4 h-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Join Call') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="evl.date > dayjs().format()"
|
||||||
|
@click="cancelEvaluation(evl)"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Ban class="w-4 h-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Cancel') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-ink-gray-5">
|
<div v-else class="text-sm italic text-ink-gray-5">
|
||||||
{{ __('No upcoming evaluations.') }}
|
{{ __('Please schedule an evaluation to get certified.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EvaluationModal
|
<EvaluationModal
|
||||||
@@ -50,15 +78,23 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
|
import {
|
||||||
import { inject, ref } from 'vue'
|
Ban,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
GraduationCap,
|
||||||
|
HeadsetIcon,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { inject, ref, getCurrentInstance } from 'vue'
|
||||||
import { formatTime } from '../utils'
|
import { formatTime } from '../utils'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource, call } from 'frappe-ui'
|
||||||
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const showEvalModal = ref(false)
|
const showEvalModal = ref(false)
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
@@ -77,10 +113,10 @@ const props = defineProps({
|
|||||||
|
|
||||||
const upcoming_evals = createResource({
|
const upcoming_evals = createResource({
|
||||||
url: 'lms.lms.utils.get_upcoming_evals',
|
url: 'lms.lms.utils.get_upcoming_evals',
|
||||||
cache: ['upcoming_evals', user.data.name],
|
|
||||||
params: {
|
params: {
|
||||||
student: user.data.name,
|
student: user.data.name,
|
||||||
courses: props.courses.map((course) => course.course),
|
courses: props.courses.map((course) => course.course),
|
||||||
|
batch: props.batch,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
@@ -88,4 +124,32 @@ const upcoming_evals = createResource({
|
|||||||
function openEvalModal() {
|
function openEvalModal() {
|
||||||
showEvalModal.value = true
|
showEvalModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openEvalCall = (evl) => {
|
||||||
|
window.open(evl.google_meet_link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEvaluation = (evl) => {
|
||||||
|
$dialog({
|
||||||
|
title: __('Cancel this evaluation?'),
|
||||||
|
message: __(
|
||||||
|
'Are you sure you want to cancel this evaluation? This action cannot be undone.'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Cancel'),
|
||||||
|
theme: 'red',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(close) {
|
||||||
|
call('lms.lms.api.cancel_evaluation', { evaluation: evl }).then(
|
||||||
|
() => {
|
||||||
|
upcoming_evals.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
|||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
import Apps from '@/components/Apps.vue'
|
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 {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LogIn,
|
LogIn,
|
||||||
@@ -75,12 +83,6 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Sun,
|
Sun,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { convertToTitleCase } from '../utils'
|
|
||||||
import { usersStore } from '@/stores/user'
|
|
||||||
import { useSettings } from '@/stores/settings'
|
|
||||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
|
||||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { logout, branding } = sessionStore()
|
const { logout, branding } = sessionStore()
|
||||||
@@ -89,6 +91,8 @@ const settingsStore = useSettings()
|
|||||||
let { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
const showSettingsModal = ref(false)
|
const showSettingsModal = ref(false)
|
||||||
const theme = ref('light')
|
const theme = ref('light')
|
||||||
|
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
|
||||||
|
const $dialog = createDialog
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isCollapsed: {
|
isCollapsed: {
|
||||||
@@ -130,6 +134,13 @@ const userDropdownOptions = computed(() => {
|
|||||||
return isLoggedIn
|
return isLoggedIn
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: theme.value === 'light' ? Moon : Sun,
|
||||||
|
label: 'Toggle Theme',
|
||||||
|
onClick: () => {
|
||||||
|
toggleTheme()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: markRaw(Apps),
|
component: markRaw(Apps),
|
||||||
condition: () => {
|
condition: () => {
|
||||||
@@ -139,13 +150,6 @@ const userDropdownOptions = computed(() => {
|
|||||||
else return false
|
else return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: theme.value === 'light' ? Moon : Sun,
|
|
||||||
label: 'Toggle Theme',
|
|
||||||
onClick: () => {
|
|
||||||
toggleTheme()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
@@ -156,6 +160,33 @@ const userDropdownOptions = computed(() => {
|
|||||||
return userResource.data?.is_moderator
|
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,
|
icon: LogOut,
|
||||||
label: 'Log out',
|
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>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="user.data?.is_moderator"
|
v-if="user.data?.is_moderator && batch.data?.certification"
|
||||||
@click="openCertificateDialog = true"
|
@click="openCertificateDialog = true"
|
||||||
>
|
>
|
||||||
{{ __('Generate Certificates') }}
|
{{ __('Generate Certificates') }}
|
||||||
@@ -21,7 +21,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="batch.data" class="grid grid-cols-[75%,25%]">
|
<div
|
||||||
|
v-if="batch.data"
|
||||||
|
class="grid grid-cols-[75%,25%] h-[calc(100vh-3.2rem)]"
|
||||||
|
>
|
||||||
<div class="border-r">
|
<div class="border-r">
|
||||||
<Tabs
|
<Tabs
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
@@ -190,8 +193,9 @@
|
|||||||
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" />
|
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
|
||||||
import { computed, inject, ref } from 'vue'
|
import { computed, inject, ref } from 'vue'
|
||||||
|
import { useRouteQuery } from '@vueuse/router'
|
||||||
|
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import {
|
import {
|
||||||
@@ -267,7 +271,7 @@ const isStudent = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabIndex = ref(0)
|
const tabIndex = useRouteQuery('tab', 0, { transform: Number })
|
||||||
const tabs = computed(() => {
|
const tabs = computed(() => {
|
||||||
let batchTabs = []
|
let batchTabs = []
|
||||||
batchTabs.push({
|
batchTabs.push({
|
||||||
@@ -310,7 +314,7 @@ const tabs = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const redirectToLogin = () => {
|
const redirectToLogin = () => {
|
||||||
window.location.href = `/login?redirect-to=/batches`
|
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAnnouncementModal = () => {
|
const openAnnouncementModal = () => {
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ const batch = createResource({
|
|||||||
batch: props.batchName,
|
batch: props.batchName,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (!data) {
|
||||||
|
router.push({ name: 'Batches' })
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const courses = createResource({
|
const courses = createResource({
|
||||||
|
|||||||
@@ -13,15 +13,14 @@
|
|||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
<div class="space-y-4 mb-4">
|
||||||
<div>
|
<FormControl
|
||||||
<FormControl
|
v-model="batch.title"
|
||||||
v-model="batch.title"
|
:label="__('Title')"
|
||||||
:label="__('Title')"
|
:required="true"
|
||||||
:required="true"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div class="flex items-center space-x-5">
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.published"
|
v-model="batch.published"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -32,6 +31,11 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:label="__('Allow self enrollment')"
|
:label="__('Allow self enrollment')"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.certification"
|
||||||
|
type="checkbox"
|
||||||
|
:label="__('Certification')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,30 +94,8 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
:filters="{ ignore_user_type: 1 }"
|
:filters="{ ignore_user_type: 1 }"
|
||||||
/>
|
/>
|
||||||
<div class="mb-4">
|
|
||||||
<FormControl
|
<div class="my-10">
|
||||||
v-model="batch.description"
|
|
||||||
:label="__('Description')"
|
|
||||||
type="textarea"
|
|
||||||
class="my-4"
|
|
||||||
:placeholder="__('Short description of the batch')"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-ink-gray-5 mb-1">
|
|
||||||
{{ __('Batch Details') }}
|
|
||||||
<span class="text-ink-red-3">*</span>
|
|
||||||
</label>
|
|
||||||
<TextEditor
|
|
||||||
:content="batch.batch_details"
|
|
||||||
@change="(val) => (batch.batch_details = val)"
|
|
||||||
:editable="true"
|
|
||||||
:fixedMenu="true"
|
|
||||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Date and Time') }}
|
{{ __('Date and Time') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +115,14 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.timezone"
|
||||||
|
:label="__('Timezone')"
|
||||||
|
type="text"
|
||||||
|
:placeholder="__('Example: IST (+5:30)')"
|
||||||
|
class="mb-4"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -149,18 +139,11 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
v-model="batch.timezone"
|
|
||||||
:label="__('Timezone')"
|
|
||||||
type="text"
|
|
||||||
:placeholder="__('Example: IST (+5:30)')"
|
|
||||||
class="mb-4"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
|
<div class="mb-10">
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -179,6 +162,11 @@
|
|||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="Email Template"
|
||||||
|
:label="__('Email Template')"
|
||||||
|
v-model="batch.confirmation_email_template"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -230,6 +218,33 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="my-10">
|
||||||
|
<div class="text-lg font-semibold mb-4">
|
||||||
|
{{ __('Description') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.description"
|
||||||
|
:label="__('Short Description')"
|
||||||
|
type="textarea"
|
||||||
|
class="my-4"
|
||||||
|
:placeholder="__('Short description of the batch')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-ink-gray-5 mb-1">
|
||||||
|
{{ __('Batch Details') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor
|
||||||
|
:content="batch.batch_details"
|
||||||
|
@change="(val) => (batch.batch_details = val)"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -278,10 +293,12 @@ const batch = reactive({
|
|||||||
end_time: '',
|
end_time: '',
|
||||||
timezone: '',
|
timezone: '',
|
||||||
evaluation_end_date: '',
|
evaluation_end_date: '',
|
||||||
|
confirmation_email_template: '',
|
||||||
seat_count: '',
|
seat_count: '',
|
||||||
medium: '',
|
medium: '',
|
||||||
category: '',
|
category: '',
|
||||||
allow_self_enrollment: false,
|
allow_self_enrollment: false,
|
||||||
|
certification: false,
|
||||||
image: null,
|
image: null,
|
||||||
paid_batch: false,
|
paid_batch: false,
|
||||||
currency: '',
|
currency: '',
|
||||||
@@ -351,7 +368,12 @@ const batchDetail = createResource({
|
|||||||
batch[key] = `${hours}:${minutes}`
|
batch[key] = `${hours}:${minutes}`
|
||||||
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
|
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
|
||||||
})
|
})
|
||||||
let checkboxes = ['published', 'paid_batch', 'allow_self_enrollment']
|
let checkboxes = [
|
||||||
|
'published',
|
||||||
|
'paid_batch',
|
||||||
|
'allow_self_enrollment',
|
||||||
|
'certification',
|
||||||
|
]
|
||||||
for (let idx in checkboxes) {
|
for (let idx in checkboxes) {
|
||||||
let key = checkboxes[idx]
|
let key = checkboxes[idx]
|
||||||
batch[key] = batch[key] ? true : false
|
batch[key] = batch[key] ? true : false
|
||||||
|
|||||||
@@ -26,13 +26,19 @@
|
|||||||
{{ __('All Batches') }}
|
{{ __('All Batches') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-2"
|
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
|
||||||
>
|
>
|
||||||
<TabButtons
|
<TabButtons
|
||||||
v-if="user.data"
|
v-if="user.data"
|
||||||
:buttons="batchTabs"
|
:buttons="batchTabs"
|
||||||
v-model="currentTab"
|
v-model="currentTab"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="certification"
|
||||||
|
:label="__('Certification')"
|
||||||
|
type="checkbox"
|
||||||
|
@change="updateBatches()"
|
||||||
|
/>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="title"
|
v-model="title"
|
||||||
@@ -111,6 +117,7 @@ const pageLength = ref(20)
|
|||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
|
const certification = ref(false)
|
||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
|
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
|
||||||
const orderBy = ref('start_date')
|
const orderBy = ref('start_date')
|
||||||
@@ -130,6 +137,7 @@ const setFiltersFromQuery = () => {
|
|||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
title.value = queries.get('title') || ''
|
title.value = queries.get('title') || ''
|
||||||
currentCategory.value = queries.get('category') || null
|
currentCategory.value = queries.get('category') || null
|
||||||
|
certification.value = queries.get('certification') || false
|
||||||
}
|
}
|
||||||
|
|
||||||
const batches = createListResource({
|
const batches = createListResource({
|
||||||
@@ -161,6 +169,7 @@ const updateBatches = () => {
|
|||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
updateCategoryFilter()
|
updateCategoryFilter()
|
||||||
updateTitleFilter()
|
updateTitleFilter()
|
||||||
|
updateCertificationFilter()
|
||||||
updateTabFilter()
|
updateTabFilter()
|
||||||
updateStudentFilter()
|
updateStudentFilter()
|
||||||
setQueryParams()
|
setQueryParams()
|
||||||
@@ -182,6 +191,14 @@ const updateTitleFilter = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateCertificationFilter = () => {
|
||||||
|
if (certification.value) {
|
||||||
|
filters.value['certification'] = 1
|
||||||
|
} else {
|
||||||
|
delete filters.value['certification']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateTabFilter = () => {
|
const updateTabFilter = () => {
|
||||||
orderBy.value = 'start_date'
|
orderBy.value = 'start_date'
|
||||||
if (!user.data) {
|
if (!user.data) {
|
||||||
@@ -222,6 +239,7 @@ const setQueryParams = () => {
|
|||||||
let filterKeys = {
|
let filterKeys = {
|
||||||
title: title.value,
|
title: title.value,
|
||||||
category: currentCategory.value,
|
category: currentCategory.value,
|
||||||
|
certification: certification.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(filterKeys).forEach((key) => {
|
Object.keys(filterKeys).forEach((key) => {
|
||||||
|
|||||||
@@ -12,20 +12,15 @@
|
|||||||
v-if="access.data?.access && orderSummary.data"
|
v-if="access.data?.access && orderSummary.data"
|
||||||
class="pt-5 pb-10 mx-5"
|
class="pt-5 pb-10 mx-5"
|
||||||
>
|
>
|
||||||
<!-- <div class="mb-5">
|
|
||||||
<div class="text-lg font-semibold">
|
|
||||||
{{ __('Address') }}
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<div class="flex flex-col lg:flex-row justify-between">
|
<div class="flex flex-col lg:flex-row justify-between">
|
||||||
<div
|
<div
|
||||||
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 text-sm font-medium lg:w-1/4"
|
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 font-medium lg:w-1/3"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between space-x-2">
|
<div class="flex items-baseline justify-between space-y-2">
|
||||||
<div class="text-ink-gray-5">
|
<div class="text-ink-gray-5">
|
||||||
{{ __('Ordered Item') }}
|
{{ __('Payment for ') }} {{ type }}:
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="leading-5">
|
||||||
{{ orderSummary.data.title }}
|
{{ orderSummary.data.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +121,7 @@
|
|||||||
<p class="text-ink-gray-5">
|
<p class="text-ink-gray-5">
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
'Make sure to enter the right billing name as the same will be used in your invoice.'
|
'Make sure to enter the correct billing name as the same will be used in your invoice.'
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
@@ -140,10 +135,10 @@
|
|||||||
<div v-else-if="access.data?.message">
|
<div v-else-if="access.data?.message">
|
||||||
<NotPermitted
|
<NotPermitted
|
||||||
:text="access.data.message"
|
:text="access.data.message"
|
||||||
:buttonLabel="
|
:buttonLabel="type == 'course' ? 'Checkout Course' : 'Checkout Batch'"
|
||||||
type == 'course' ? 'Checkout Courses' : 'Checkout Batches'
|
:buttonLink="
|
||||||
|
type == 'course' ? `/lms/courses/${name}` : `/lms/batches/${name}`
|
||||||
"
|
"
|
||||||
:buttonLink="type == 'course' ? '/lms/courses' : '/lms/batches'"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!user.data?.name">
|
<div v-else-if="!user.data?.name">
|
||||||
@@ -163,7 +158,7 @@ import {
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { reactive, inject, onMounted, ref } from 'vue'
|
import { reactive, inject, onMounted, computed } from 'vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import NotPermitted from '@/components/NotPermitted.vue'
|
import NotPermitted from '@/components/NotPermitted.vue'
|
||||||
import { showToast } from '@/utils/'
|
import { showToast } from '@/utils/'
|
||||||
@@ -193,7 +188,7 @@ const props = defineProps({
|
|||||||
const access = createResource({
|
const access = createResource({
|
||||||
url: 'lms.lms.api.validate_billing_access',
|
url: 'lms.lms.api.validate_billing_access',
|
||||||
params: {
|
params: {
|
||||||
type: props.type,
|
billing_type: props.type,
|
||||||
name: props.name,
|
name: props.name,
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
@@ -206,7 +201,7 @@ const orderSummary = createResource({
|
|||||||
url: 'lms.lms.utils.get_order_summary',
|
url: 'lms.lms.utils.get_order_summary',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||||
docname: props.name,
|
docname: props.name,
|
||||||
country: billingDetails.country,
|
country: billingDetails.country,
|
||||||
}
|
}
|
||||||
@@ -236,22 +231,26 @@ const paymentLink = createResource({
|
|||||||
url: 'lms.lms.payments.get_payment_link',
|
url: 'lms.lms.payments.get_payment_link',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||||
docname: props.name,
|
docname: props.name,
|
||||||
title: orderSummary.data.title,
|
title: orderSummary.data.title,
|
||||||
amount: orderSummary.data.original_amount,
|
amount: orderSummary.data.original_amount,
|
||||||
total_amount: orderSummary.data.amount,
|
total_amount: orderSummary.data.amount,
|
||||||
currency: orderSummary.data.currency,
|
currency: orderSummary.data.currency,
|
||||||
address: billingDetails,
|
address: billingDetails,
|
||||||
|
redirect_to: redirectTo.value,
|
||||||
|
payment_for_certificate: props.type == 'certificate',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const generatePaymentLink = () => {
|
const generatePaymentLink = () => {
|
||||||
|
console.log('called')
|
||||||
paymentLink.submit(
|
paymentLink.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
|
console.log('validation start')
|
||||||
if (!billingDetails.source) {
|
if (!billingDetails.source) {
|
||||||
return __('Please let us know where you heard about us from.')
|
return __('Please let us know where you heard about us from.')
|
||||||
}
|
}
|
||||||
@@ -330,6 +329,8 @@ const validateAddress = () => {
|
|||||||
!states.includes(billingDetails.state)
|
!states.includes(billingDetails.state)
|
||||||
)
|
)
|
||||||
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
|
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
|
||||||
|
|
||||||
|
console.log('validation address')
|
||||||
}
|
}
|
||||||
|
|
||||||
const showError = (err) => {
|
const showError = (err) => {
|
||||||
@@ -347,4 +348,14 @@ const changeCurrency = (country) => {
|
|||||||
billingDetails.country = country
|
billingDetails.country = country
|
||||||
orderSummary.reload()
|
orderSummary.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const redirectTo = computed(() => {
|
||||||
|
if (props.type == 'course') {
|
||||||
|
return `/lms/courses/${props.name}`
|
||||||
|
} else if (props.type == 'batch') {
|
||||||
|
return `/lms/batches/${props.name}`
|
||||||
|
} else if (props.type == 'certificate') {
|
||||||
|
return `/lms/courses/${props.name}/certification`
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
117
frontend/src/pages/CourseCertification.vue
Normal file
117
frontend/src/pages/CourseCertification.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
</header>
|
||||||
|
<div class="p-5">
|
||||||
|
<div v-if="certificate.data && Object.keys(certificate.data).length">
|
||||||
|
<div class="text-lg text-ink-gray-9 font-semibold mb-1">
|
||||||
|
{{ __('Certification') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-9 text-sm">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'You are already certified for this course. Click on the card below to open your certificate.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border p-3 w-fit min-w-60 rounded-md space-y-2 hover:bg-surface-gray-1 cursor-pointer mt-5"
|
||||||
|
@click="openCertificate(certificate.data)"
|
||||||
|
>
|
||||||
|
<div class="text-ink-gray-9 font-semibold">
|
||||||
|
{{ courseTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-ink-gray-7 font-medium">
|
||||||
|
{{ __('Issued On') }}:
|
||||||
|
{{ dayjs(certificate.data.issue_date).format('DD MMM YYYY') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<UpcomingEvaluations v-if="courses.length" :courses="courses" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
import { Breadcrumbs, call, createResource } from 'frappe-ui'
|
||||||
|
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||||
|
|
||||||
|
const courseTitle = ref(null)
|
||||||
|
const evaluator = ref(null)
|
||||||
|
const courses = ref([])
|
||||||
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCourseDetails()
|
||||||
|
})
|
||||||
|
|
||||||
|
const certificate = createResource({
|
||||||
|
url: 'frappe.client.get_value',
|
||||||
|
params: {
|
||||||
|
doctype: 'LMS Certificate',
|
||||||
|
filters: {
|
||||||
|
member: user.data?.name,
|
||||||
|
course: props.courseName,
|
||||||
|
},
|
||||||
|
fieldname: ['name', 'template', 'issue_date'],
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: [user.data?.name, props.courseName],
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCourseDetails = () => {
|
||||||
|
call('frappe.client.get_value', {
|
||||||
|
doctype: 'LMS Course',
|
||||||
|
filters: { name: props.courseName },
|
||||||
|
fieldname: ['title', 'evaluator'],
|
||||||
|
}).then((data) => {
|
||||||
|
courseTitle.value = data.title
|
||||||
|
evaluator.value = data.evaluator
|
||||||
|
populateCourses()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const populateCourses = () => {
|
||||||
|
courses.value = [
|
||||||
|
{
|
||||||
|
course: props.courseName,
|
||||||
|
title: courseTitle.value,
|
||||||
|
evaluator: evaluator.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCertificate = (certificate) => {
|
||||||
|
window.open(
|
||||||
|
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||||
|
certificate.name
|
||||||
|
}&format=${encodeURIComponent(certificate.template)}`,
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => [
|
||||||
|
{
|
||||||
|
label: __('Courses'),
|
||||||
|
route: { name: 'Courses' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: courseTitle.value,
|
||||||
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Certification'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5 mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-10 mb-4">
|
<div class="grid grid-cols-2 gap-10 mb-4">
|
||||||
<div
|
<div
|
||||||
v-if="user.data?.is_moderator"
|
v-if="user.data?.is_moderator"
|
||||||
class="flex flex-col space-y-4"
|
class="flex flex-col space-y-4"
|
||||||
@@ -188,43 +188,48 @@
|
|||||||
v-model="course.featured"
|
v-model="course.featured"
|
||||||
:label="__('Featured')"
|
:label="__('Featured')"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="flex flex-col space-y-3">
|
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.disable_self_learning"
|
v-model="course.disable_self_learning"
|
||||||
:label="__('Disable Self Enrollment')"
|
:label="__('Disable Self Enrollment')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
type="checkbox"
|
|
||||||
v-model="course.enable_certification"
|
|
||||||
:label="__('Completion Certificate')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container border-t">
|
<div class="container border-t space-y-4">
|
||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5">
|
||||||
{{ __('Pricing') }}
|
{{ __('Pricing and Certification') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="grid grid-cols-3">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.paid_course"
|
v-model="course.paid_course"
|
||||||
:label="__('Paid Course')"
|
:label="__('Paid Course')"
|
||||||
/>
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.enable_certification"
|
||||||
|
:label="__('Completion Certificate')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.paid_certificate"
|
||||||
|
:label="__('Paid Certificate')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl v-model="course.course_price" :label="__('Amount')" />
|
||||||
v-model="course.course_price"
|
|
||||||
:label="__('Course Price')"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
<Link
|
<Link
|
||||||
doctype="Currency"
|
doctype="Currency"
|
||||||
v-model="course.currency"
|
v-model="course.currency"
|
||||||
:filters="{ enabled: 1 }"
|
:filters="{ enabled: 1 }"
|
||||||
:label="__('Currency')"
|
:label="__('Currency')"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="course.paid_certificate"
|
||||||
|
doctype="Course Evaluator"
|
||||||
|
v-model="course.evaluator"
|
||||||
|
:label="__('Evaluator')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,8 +301,10 @@ const course = reactive({
|
|||||||
disable_self_learning: false,
|
disable_self_learning: false,
|
||||||
enable_certification: false,
|
enable_certification: false,
|
||||||
paid_course: false,
|
paid_course: false,
|
||||||
|
paid_certificate: false,
|
||||||
course_price: '',
|
course_price: '',
|
||||||
currency: '',
|
currency: '',
|
||||||
|
evaluator: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -391,6 +398,7 @@ const courseResource = createResource({
|
|||||||
'paid_course',
|
'paid_course',
|
||||||
'featured',
|
'featured',
|
||||||
'enable_certification',
|
'enable_certification',
|
||||||
|
'paid_certifiate',
|
||||||
]
|
]
|
||||||
for (let idx in checkboxes) {
|
for (let idx in checkboxes) {
|
||||||
let key = checkboxes[idx]
|
let key = checkboxes[idx]
|
||||||
|
|||||||
@@ -1,316 +1,325 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="courses.data">
|
<header
|
||||||
<header
|
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
>
|
||||||
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
<router-link
|
||||||
|
v-if="user.data?.is_moderator"
|
||||||
|
:to="{
|
||||||
|
name: 'CourseForm',
|
||||||
|
params: { courseName: 'new' },
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<Breadcrumbs
|
<Button variant="solid">
|
||||||
class="h-7"
|
<template #prefix>
|
||||||
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
|
<Plus class="h-4 w-4 stroke-1.5" />
|
||||||
/>
|
</template>
|
||||||
<div class="flex space-x-2 justify-end">
|
{{ __('New') }}
|
||||||
<div class="w-40 md:w-44">
|
</Button>
|
||||||
<FormControl
|
</router-link>
|
||||||
v-if="categories.data?.length"
|
</header>
|
||||||
type="select"
|
<div class="p-5 pb-10">
|
||||||
v-model="currentCategory"
|
<div
|
||||||
:options="categories.data"
|
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
|
||||||
:placeholder="__('Category')"
|
>
|
||||||
/>
|
<div class="text-lg font-semibold">
|
||||||
</div>
|
{{ __('All Courses') }}
|
||||||
<div class="w-28 md:w-36">
|
</div>
|
||||||
|
<div
|
||||||
|
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="courseTabs"
|
||||||
|
v-model="currentTab"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="certification"
|
||||||
|
:label="__('Certification')"
|
||||||
|
type="checkbox"
|
||||||
|
@change="updateCourses()"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
v-model="title"
|
||||||
|
:placeholder="__('Search by Title')"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||||
v-model="searchQuery"
|
@input="updateCourses()"
|
||||||
@input="courses.reload()"
|
/>
|
||||||
>
|
<div class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40">
|
||||||
<template #prefix>
|
<Select
|
||||||
<Search
|
v-if="categories.length"
|
||||||
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
|
v-model="currentCategory"
|
||||||
name="search"
|
:options="categories"
|
||||||
/>
|
:placeholder="__('Category')"
|
||||||
</template>
|
@change="updateCourses()"
|
||||||
</FormControl>
|
/>
|
||||||
</div>
|
|
||||||
<router-link
|
|
||||||
v-if="user.data?.is_moderator || user.data?.is_instructor"
|
|
||||||
:to="{
|
|
||||||
name: 'CourseForm',
|
|
||||||
params: {
|
|
||||||
courseName: 'new',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Button variant="solid">
|
|
||||||
<template #prefix>
|
|
||||||
<Plus class="h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
{{ __('New') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="">
|
|
||||||
<Tabs
|
|
||||||
v-if="hasCourses"
|
|
||||||
as="div"
|
|
||||||
v-model="tabIndex"
|
|
||||||
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
|
||||||
:tabs="makeTabs"
|
|
||||||
>
|
|
||||||
<template #tab="{ tab, selected }">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="group -mb-px flex items-center gap-2 overflow-hidden border-b border-transparent py-2.5 text-base text-ink-gray-5 duration-300 ease-in-out hover:border-outline-gray-3 hover:text-ink-gray-9"
|
|
||||||
:class="{ 'text-ink-gray-9': selected }"
|
|
||||||
>
|
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
|
||||||
{{ __(tab.label) }}
|
|
||||||
<Badge theme="gray">
|
|
||||||
{{ tab.count }}
|
|
||||||
</Badge>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<template #tab-panel="{ tab }">
|
|
||||||
<div
|
|
||||||
v-if="tab.courses && tab.courses.value.length"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 3xl:grid-cols-5 gap-7 my-5 mx-5"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-for="course in tab.courses.value"
|
|
||||||
:to="
|
|
||||||
course.membership && course.current_lesson
|
|
||||||
? {
|
|
||||||
name: 'Lesson',
|
|
||||||
params: {
|
|
||||||
courseName: course.name,
|
|
||||||
chapterNumber: course.current_lesson.split('-')[0],
|
|
||||||
lessonNumber: course.current_lesson.split('-')[1],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: course.membership
|
|
||||||
? {
|
|
||||||
name: 'Lesson',
|
|
||||||
params: {
|
|
||||||
courseName: course.name,
|
|
||||||
chapterNumber: 1,
|
|
||||||
lessonNumber: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { courseName: course.name },
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<CourseCard :course="course" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div v-else class="p-5 italic text-ink-gray-4">
|
|
||||||
{{ __('No {0} courses').format(tab.label.toLowerCase()) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tabs>
|
|
||||||
<div
|
|
||||||
v-else-if="
|
|
||||||
!courses.loading &&
|
|
||||||
(user.data?.is_moderator || user.data?.is_instructor)
|
|
||||||
"
|
|
||||||
class="grid grid-cols-3 p-5"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'CourseForm',
|
|
||||||
params: {
|
|
||||||
courseName: 'new',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="bg-surface-menu-bar py-32 px-5 rounded-md">
|
|
||||||
<div class="flex flex-col items-center text-center space-y-2">
|
|
||||||
<Plus
|
|
||||||
class="size-10 stroke-1 text-ink-gray-8 p-1 rounded-full border bg-surface-white"
|
|
||||||
/>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ __('Create a Course') }}
|
|
||||||
</div>
|
|
||||||
<span class="text-ink-gray-7 text-sm leading-4">
|
|
||||||
{{ __('You can add chapters and lessons to it.') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="!courses.loading && !hasCourses"
|
|
||||||
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
|
||||||
>
|
|
||||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
|
||||||
<div class="text-xl font-medium">
|
|
||||||
{{ __('No courses found') }}
|
|
||||||
</div>
|
|
||||||
<div class="leading-5">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'There are no courses available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="courses.data?.length"
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-5"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-for="course in courses.data"
|
||||||
|
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"
|
||||||
|
>
|
||||||
|
<CourseCard :course="course" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!courses.list.loading"
|
||||||
|
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 italic mt-48"
|
||||||
|
>
|
||||||
|
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||||
|
<div class="text-lg font-medium mb-1">
|
||||||
|
{{ __('No courses found') }}
|
||||||
|
</div>
|
||||||
|
<div class="leading-5 w-2/5 text-center">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'There are no courses matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!courses.list.loading && courses.hasNextPage"
|
||||||
|
class="flex justify-center mt-5"
|
||||||
|
>
|
||||||
|
<Button @click="courses.next()">
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
call,
|
createListResource,
|
||||||
createResource,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
Tabs,
|
Select,
|
||||||
|
TabButtons,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { BookOpen, Plus, Search } from 'lucide-vue-next'
|
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||||
import { ref, computed, inject, onMounted, watch } from 'vue'
|
|
||||||
import { updateDocumentTitle } from '@/utils'
|
import { updateDocumentTitle } from '@/utils'
|
||||||
import { useRouter } from 'vue-router'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
import { useSettings } from '@/stores/settings'
|
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const searchQuery = ref('')
|
const dayjs = inject('$dayjs')
|
||||||
|
const start = ref(0)
|
||||||
|
const pageLength = ref(30)
|
||||||
|
const categories = ref([])
|
||||||
const currentCategory = ref(null)
|
const currentCategory = ref(null)
|
||||||
const hasCourses = ref(false)
|
const title = ref('')
|
||||||
const router = useRouter()
|
const certification = ref(false)
|
||||||
const settings = useSettings()
|
const filters = ref({})
|
||||||
|
const currentTab = ref('Live')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkLearningPath()
|
setFiltersFromQuery()
|
||||||
let queries = new URLSearchParams(location.search)
|
updateCourses()
|
||||||
if (queries.has('category')) {
|
categories.value = [
|
||||||
currentCategory.value = queries.get('category')
|
{
|
||||||
}
|
label: '',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkLearningPath = () => {
|
const setFiltersFromQuery = () => {
|
||||||
if (
|
let queries = new URLSearchParams(location.search)
|
||||||
settings.learningPaths.data &&
|
title.value = queries.get('title') || ''
|
||||||
(!user.data?.is_moderator || !user.data?.is_instructor)
|
currentCategory.value = queries.get('category') || null
|
||||||
) {
|
certification.value = queries.get('certification') || false
|
||||||
router.push({ name: 'Programs' })
|
}
|
||||||
|
|
||||||
|
const courses = createListResource({
|
||||||
|
doctype: 'LMS Course',
|
||||||
|
url: 'lms.lms.utils.get_courses',
|
||||||
|
cache: ['courses', user.data?.name],
|
||||||
|
pageLength: pageLength.value,
|
||||||
|
start: start.value,
|
||||||
|
onSuccess(data) {
|
||||||
|
let allCategories = data.map((course) => course.category)
|
||||||
|
allCategories = allCategories.filter(
|
||||||
|
(category, index) => allCategories.indexOf(category) === index && category
|
||||||
|
)
|
||||||
|
if (categories.value.length <= allCategories.length) {
|
||||||
|
updateCategories(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateCourses = () => {
|
||||||
|
updateFilters()
|
||||||
|
courses.update({
|
||||||
|
filters: filters.value,
|
||||||
|
})
|
||||||
|
courses.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFilters = () => {
|
||||||
|
updateCategoryFilter()
|
||||||
|
updateTitleFilter()
|
||||||
|
updateCertificationFilter()
|
||||||
|
updateTabFilter()
|
||||||
|
updateStudentFilter()
|
||||||
|
setQueryParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCategoryFilter = () => {
|
||||||
|
if (currentCategory.value) {
|
||||||
|
filters.value['category'] = currentCategory.value
|
||||||
|
} else {
|
||||||
|
delete filters.value['category']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const courses = createResource({
|
const updateTitleFilter = () => {
|
||||||
url: 'lms.lms.utils.get_courses',
|
if (title.value) {
|
||||||
cache: ['courses', user.data?.email],
|
filters.value['title'] = ['like', `%${title.value}%`]
|
||||||
auto: true,
|
} else {
|
||||||
|
delete filters.value['title']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCertificationFilter = () => {
|
||||||
|
if (certification.value) {
|
||||||
|
filters.value['certification'] = 1
|
||||||
|
} else {
|
||||||
|
delete filters.value['certification']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTabFilter = () => {
|
||||||
|
if (!user.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete filters.value['live']
|
||||||
|
delete filters.value['created']
|
||||||
|
delete filters.value['published_on']
|
||||||
|
delete filters.value['upcoming']
|
||||||
|
|
||||||
|
if (currentTab.value == 'Enrolled' && user.data?.is_student) {
|
||||||
|
filters.value['enrolled'] = 1
|
||||||
|
delete filters.value['published']
|
||||||
|
} else {
|
||||||
|
delete filters.value['published']
|
||||||
|
delete filters.value['enrolled']
|
||||||
|
|
||||||
|
if (currentTab.value == 'Live') {
|
||||||
|
filters.value['published'] = 1
|
||||||
|
filters.value['upcoming'] = 0
|
||||||
|
filters.value['live'] = 1
|
||||||
|
} else if (currentTab.value == 'Upcoming') {
|
||||||
|
filters.value['upcoming'] = 1
|
||||||
|
filters.value['published'] = 1
|
||||||
|
} else if (currentTab.value == 'New') {
|
||||||
|
filters.value['published'] = 1
|
||||||
|
filters.value['published_on'] = [
|
||||||
|
'>=',
|
||||||
|
dayjs().add(-3, 'month').format('YYYY-MM-DD'),
|
||||||
|
]
|
||||||
|
} else if (currentTab.value == 'Created') {
|
||||||
|
filters.value['created'] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStudentFilter = () => {
|
||||||
|
if (!user.data || (user.data?.is_student && currentTab.value != 'Enrolled')) {
|
||||||
|
filters.value['published'] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQueryParams = () => {
|
||||||
|
let queries = new URLSearchParams(location.search)
|
||||||
|
let filterKeys = {
|
||||||
|
title: title.value,
|
||||||
|
category: currentCategory.value,
|
||||||
|
certification: certification.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(filterKeys).forEach((key) => {
|
||||||
|
if (filterKeys[key]) {
|
||||||
|
queries.set(key, filterKeys[key])
|
||||||
|
} else {
|
||||||
|
queries.delete(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
history.replaceState({}, '', `${location.pathname}?${queries.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCategories = (data) => {
|
||||||
|
data.forEach((course) => {
|
||||||
|
if (
|
||||||
|
course.category &&
|
||||||
|
!categories.value.find((category) => category.value === course.category)
|
||||||
|
)
|
||||||
|
categories.value.push({
|
||||||
|
label: course.category,
|
||||||
|
value: course.category,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentTab, () => {
|
||||||
|
updateCourses()
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabIndex = ref(0)
|
const courseType = computed(() => {
|
||||||
let tabs
|
let types = [
|
||||||
|
{ label: __(''), value: null },
|
||||||
|
{ label: __('New'), value: 'New' },
|
||||||
|
{ label: __('Upcoming'), value: 'Upcoming' },
|
||||||
|
]
|
||||||
|
if (user.data?.is_student) {
|
||||||
|
types.push({ label: __('Enrolled'), value: 'Enrolled' })
|
||||||
|
} else {
|
||||||
|
types.push({ label: __('Created'), value: 'Created' })
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
})
|
||||||
|
|
||||||
const makeTabs = computed(() => {
|
const courseTabs = computed(() => {
|
||||||
tabs = []
|
let tabs = [
|
||||||
addToTabs('Live')
|
{
|
||||||
addToTabs('New')
|
label: __('Live'),
|
||||||
addToTabs('Upcoming')
|
},
|
||||||
|
{
|
||||||
if (user.data) {
|
label: __('New'),
|
||||||
addToTabs('Enrolled')
|
},
|
||||||
|
{
|
||||||
if (
|
label: __('Upcoming'),
|
||||||
user.data.is_moderator ||
|
},
|
||||||
user.data.is_instructor ||
|
]
|
||||||
courses.data?.created?.length
|
if (user.data?.is_student) {
|
||||||
) {
|
tabs.push({ label: __('Enrolled') })
|
||||||
addToTabs('Created')
|
} else {
|
||||||
}
|
tabs.push({ label: __('Created') })
|
||||||
|
|
||||||
if (user.data.is_moderator) {
|
|
||||||
addToTabs('Under Review')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return tabs
|
return tabs
|
||||||
})
|
})
|
||||||
|
|
||||||
const addToTabs = (label) => {
|
const breadcrumbs = computed(() => [
|
||||||
let courses = getCourses(label.toLowerCase().split(' ').join('_'))
|
{
|
||||||
tabs.push({
|
label: __('Courses'),
|
||||||
label,
|
route: { name: 'Courses' },
|
||||||
courses: computed(() => courses),
|
|
||||||
count: computed(() => courses.length),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCourses = (type) => {
|
|
||||||
let courseList = courses.data[type]
|
|
||||||
if (searchQuery.value) {
|
|
||||||
let query = searchQuery.value.toLowerCase()
|
|
||||||
courseList = courseList.filter(
|
|
||||||
(course) =>
|
|
||||||
course.title.toLowerCase().includes(query) ||
|
|
||||||
course.short_introduction.toLowerCase().includes(query) ||
|
|
||||||
course.tags.filter((tag) => tag.toLowerCase().includes(query)).length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (currentCategory.value && currentCategory.value != '') {
|
|
||||||
courseList = courseList.filter(
|
|
||||||
(course) => course.category == currentCategory.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return courseList
|
|
||||||
}
|
|
||||||
|
|
||||||
const categories = createResource({
|
|
||||||
url: 'lms.lms.api.get_categories',
|
|
||||||
makeParams() {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Course',
|
|
||||||
filters: {
|
|
||||||
published: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
cache: ['courseCategories'],
|
])
|
||||||
auto: true,
|
|
||||||
transform(data) {
|
|
||||||
data.unshift({
|
|
||||||
label: '',
|
|
||||||
value: null,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(courses, () => {
|
|
||||||
if (courses.data) {
|
|
||||||
Object.keys(courses.data).forEach((section) => {
|
|
||||||
if (courses.data[section].length) {
|
|
||||||
hasCourses.value = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => currentCategory.value,
|
|
||||||
() => {
|
|
||||||
let queries = new URLSearchParams(location.search)
|
|
||||||
if (currentCategory.value) {
|
|
||||||
queries.set('category', currentCategory.value)
|
|
||||||
} else {
|
|
||||||
queries.delete('category')
|
|
||||||
}
|
|
||||||
history.pushState(null, '', `${location.pathname}?${queries.toString()}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const pageMeta = computed(() => {
|
const pageMeta = computed(() => {
|
||||||
return {
|
return {
|
||||||
title: 'Courses',
|
title: 'Courses',
|
||||||
description: 'All Courses divided by categories',
|
description: 'All published courses.',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
<CertificationLinks :courseName="courseName" />
|
||||||
</header>
|
</header>
|
||||||
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
||||||
<div
|
<div
|
||||||
@@ -197,13 +198,14 @@ import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
|||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { ChevronLeft, ChevronRight, GraduationCap } from 'lucide-vue-next'
|
||||||
import Discussions from '@/components/Discussions.vue'
|
import Discussions from '@/components/Discussions.vue'
|
||||||
import { getEditorTools, updateDocumentTitle } from '../utils'
|
import { getEditorTools, updateDocumentTitle } from '../utils'
|
||||||
import EditorJS from '@editorjs/editorjs'
|
import EditorJS from '@editorjs/editorjs'
|
||||||
import LessonContent from '@/components/LessonContent.vue'
|
import LessonContent from '@/components/LessonContent.vue'
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import ProgressBar from '@/components/ProgressBar.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const renderEditor = (holder) => {
|
|||||||
const lesson = reactive({
|
const lesson = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
include_in_preview: false,
|
include_in_preview: false,
|
||||||
body: 'Test',
|
body: '',
|
||||||
instructor_notes: '',
|
instructor_notes: '',
|
||||||
content: '',
|
content: '',
|
||||||
})
|
})
|
||||||
@@ -294,7 +294,7 @@ const convertToJSON = (lessonData) => {
|
|||||||
type: 'upload',
|
type: 'upload',
|
||||||
data: {
|
data: {
|
||||||
file_url: video,
|
file_url: video,
|
||||||
file_type: 'video',
|
file_type: video.split('.').pop(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (block.includes('{{ Audio')) {
|
} else if (block.includes('{{ Audio')) {
|
||||||
@@ -303,7 +303,7 @@ const convertToJSON = (lessonData) => {
|
|||||||
type: 'upload',
|
type: 'upload',
|
||||||
data: {
|
data: {
|
||||||
file_url: audio,
|
file_url: audio,
|
||||||
file_type: 'audio',
|
file_type: audio.split('.').pop(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (block.includes('{{ PDF')) {
|
} else if (block.includes('{{ PDF')) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const roles = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const updateRole = createResource({
|
const updateRole = createResource({
|
||||||
url: 'lms.overrides.user.save_role',
|
url: 'lms.lms.api.save_role',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
user: props.profile.data?.name,
|
user: props.profile.data?.name,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
type="number"
|
type="number"
|
||||||
v-model="quiz.max_attempts"
|
v-model="quiz.max_attempts"
|
||||||
:label="__('Maximun Attempts')"
|
:label="__('Maximum Attempts')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ const routes = [
|
|||||||
component: () => import('@/pages/Lesson.vue'),
|
component: () => import('@/pages/Lesson.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/courses/:courseName/certification',
|
||||||
|
name: 'CourseCertification',
|
||||||
|
component: () => import('@/pages/CourseCertification.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/courses/:courseName/learn/:chapterName',
|
path: '/courses/:courseName/learn/:chapterName',
|
||||||
name: 'SCORMChapter',
|
name: 'SCORMChapter',
|
||||||
@@ -219,7 +225,7 @@ let router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
const { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
const { allowGuestAccess } = useSettings()
|
const { allowGuestAccess } = useSettings()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -441,6 +441,22 @@ export function getTimezones() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUserTimezone() {
|
||||||
|
try {
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
const supportedTimezones = getTimezones()
|
||||||
|
|
||||||
|
if (supportedTimezones.includes(timezone)) {
|
||||||
|
return timezone // e.g., 'Asia/Calcutta', 'America/New_York', etc.
|
||||||
|
} else {
|
||||||
|
throw Error('unsupported timezone')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting timezone:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getSidebarLinks() {
|
export function getSidebarLinks() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,10 +50,18 @@ export class Markdown {
|
|||||||
this.wrapper.innerHTML = this.text
|
this.wrapper.innerHTML = this.text
|
||||||
|
|
||||||
this.wrapper.addEventListener('keydown', (event) => {
|
this.wrapper.addEventListener('keydown', (event) => {
|
||||||
const value = event.target.textContent
|
let value = event.target.textContent
|
||||||
if (event.keyCode === 32 && value.startsWith('#')) {
|
if (event.keyCode === 32 && value.startsWith('#')) {
|
||||||
this.convertToHeader(event, value)
|
this.convertToHeader(event, value)
|
||||||
} else if (event.keyCode === 13) {
|
} else if (event.keyCode == 189) {
|
||||||
|
this.convertBlock('list', {
|
||||||
|
style: 'unordered',
|
||||||
|
})
|
||||||
|
} else if (/^[a-zA-Z]/.test(event.key)) {
|
||||||
|
this.convertBlock('paragraph', {
|
||||||
|
text: value,
|
||||||
|
})
|
||||||
|
} else if (event.keyCode === 13 || event.keyCode === 190) {
|
||||||
this.parseContent(event)
|
this.parseContent(event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -75,7 +83,11 @@ export class Markdown {
|
|||||||
|
|
||||||
parseContent(event) {
|
parseContent(event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const previousLine = this.wrapper.textContent
|
let previousLine = this.wrapper.textContent
|
||||||
|
if (event.keyCode === 190) {
|
||||||
|
previousLine = previousLine + '.'
|
||||||
|
}
|
||||||
|
|
||||||
if (previousLine && this.hasImage(previousLine)) {
|
if (previousLine && this.hasImage(previousLine)) {
|
||||||
this.wrapper.textContent = ''
|
this.wrapper.textContent = ''
|
||||||
this.convertBlock('image')
|
this.convertBlock('image')
|
||||||
@@ -94,12 +106,12 @@ export class Markdown {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
} else if (previousLine && previousLine.startsWith('1. ')) {
|
} else if (previousLine && previousLine.startsWith('1.')) {
|
||||||
this.convertBlock('list', {
|
this.convertBlock('list', {
|
||||||
style: 'ordered',
|
style: 'ordered',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
content: previousLine.replace('1. ', ''),
|
content: previousLine.replace('1.', ''),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -108,6 +120,10 @@ export class Markdown {
|
|||||||
this.convertBlock('embed', {
|
this.convertBlock('embed', {
|
||||||
source: previousLine,
|
source: previousLine,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this.convertBlock('paragraph', {
|
||||||
|
text: previousLine,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createApp, h } from 'vue'
|
|||||||
import { usersStore } from '../stores/user'
|
import { usersStore } from '../stores/user'
|
||||||
import translationPlugin from '../translation'
|
import translationPlugin from '../translation'
|
||||||
import { CircleHelp } from 'lucide-vue-next'
|
import { CircleHelp } from 'lucide-vue-next'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
export class Quiz {
|
export class Quiz {
|
||||||
constructor({ data, api, readOnly }) {
|
constructor({ data, api, readOnly }) {
|
||||||
@@ -46,6 +47,7 @@ export class Quiz {
|
|||||||
quiz: quiz,
|
quiz: quiz,
|
||||||
})
|
})
|
||||||
app.use(translationPlugin)
|
app.use(translationPlugin)
|
||||||
|
app.use(router)
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
app.provide('$user', userResource)
|
app.provide('$user', userResource)
|
||||||
app.mount(this.wrapper)
|
app.mount(this.wrapper)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ module.exports = {
|
|||||||
1.5: '1.5',
|
1.5: '1.5',
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
'2xl': '1536px',
|
'2xl': '1600px',
|
||||||
'3xl': '1920px',
|
'3xl': '1920px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['fs'],
|
allowedHosts: ['fs', 'bs'],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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.26.0"
|
||||||
|
|||||||
@@ -951,62 +951,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": null,
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payments_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "custom_css",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Payments",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.329032",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payments_tab",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
@@ -1119,230 +1063,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payment_gateway",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "accept_payment",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Payment Gateway",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.408659",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payment_gateway",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": "Payment Gateway",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "Buy Now",
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payment_button_label",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payment_gateway",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Button Label",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.439246",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payment_button_label",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": null,
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payments_cb",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payment_button_help",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": null,
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.491696",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payments_cb",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "eval:doc.accept_payment && !doc.amount_based_on_field",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "amount_field",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Amount",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.569591",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-amount",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
@@ -1399,174 +1119,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "payment_button_help",
|
|
||||||
"fieldtype": "Text",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payment_button_label",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Button Help",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.466744",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-payment_button_help",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount_based_on_field",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "payments_cb",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Amount Based On Field",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.517344",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-amount_based_on_field",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "eval:doc.accept_payment && doc.amount_based_on_field",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount_field",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "amount_based_on_field",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Amount Field",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.543136",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-amount_field",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": null,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
@@ -1679,62 +1231,6 @@
|
|||||||
"unique": 0,
|
"unique": 0,
|
||||||
"width": null
|
"width": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"collapsible_depends_on": null,
|
|
||||||
"columns": 0,
|
|
||||||
"default": null,
|
|
||||||
"depends_on": "accept_payment",
|
|
||||||
"description": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Custom Field",
|
|
||||||
"dt": "Web Form",
|
|
||||||
"fetch_from": null,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "currency",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"hide_border": 0,
|
|
||||||
"hide_days": 0,
|
|
||||||
"hide_seconds": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_preview": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"insert_after": "amount",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
"is_virtual": 0,
|
|
||||||
"label": "Currency",
|
|
||||||
"length": 0,
|
|
||||||
"link_filters": null,
|
|
||||||
"mandatory_depends_on": null,
|
|
||||||
"modified": "2024-09-19 08:35:17.595419",
|
|
||||||
"module": null,
|
|
||||||
"name": "Web Form-currency",
|
|
||||||
"no_copy": 0,
|
|
||||||
"non_negative": 0,
|
|
||||||
"options": "Currency",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": null,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_depends_on": null,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"show_dashboard": 0,
|
|
||||||
"sort_options": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
|
|||||||
12
lms/hooks.py
12
lms/hooks.py
@@ -88,7 +88,6 @@ setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
|||||||
# Override standard doctype classes
|
# Override standard doctype classes
|
||||||
|
|
||||||
override_doctype_class = {
|
override_doctype_class = {
|
||||||
"User": "lms.overrides.user.CustomUser",
|
|
||||||
"Web Template": "lms.overrides.web_template.CustomWebTemplate",
|
"Web Template": "lms.overrides.web_template.CustomWebTemplate",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +103,10 @@ doc_events = {
|
|||||||
},
|
},
|
||||||
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
|
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
|
||||||
"Notification Log": {"on_change": "lms.lms.utils.publish_notifications"},
|
"Notification Log": {"on_change": "lms.lms.utils.publish_notifications"},
|
||||||
|
"User": {
|
||||||
|
"validate": "lms.lms.user.validate_username_duplicates",
|
||||||
|
"after_insert": "lms.lms.user.after_insert",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
@@ -112,10 +115,13 @@ scheduler_events = {
|
|||||||
"hourly": [
|
"hourly": [
|
||||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||||
"lms.lms.api.update_course_statistics",
|
"lms.lms.api.update_course_statistics",
|
||||||
|
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.mark_eval_as_completed",
|
||||||
],
|
],
|
||||||
"daily": [
|
"daily": [
|
||||||
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
|
||||||
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
||||||
|
"lms.lms.doctype.lms_batch.lms_batch.send_batch_start_reminder",
|
||||||
|
"lms.lms.doctype.lms_live_class.lms_live_class.send_live_class_reminder",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +194,8 @@ jinja = {
|
|||||||
"lms.lms.utils.get_lesson_index",
|
"lms.lms.utils.get_lesson_index",
|
||||||
"lms.lms.utils.get_lesson_url",
|
"lms.lms.utils.get_lesson_url",
|
||||||
"lms.page_renderers.get_profile_url",
|
"lms.page_renderers.get_profile_url",
|
||||||
"lms.overrides.user.get_palette",
|
|
||||||
"lms.lms.utils.is_instructor",
|
"lms.lms.utils.is_instructor",
|
||||||
|
"lms.lms.utils.get_palette",
|
||||||
],
|
],
|
||||||
"filters": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
@@ -236,8 +242,6 @@ profile_url_prefix = "/users/"
|
|||||||
|
|
||||||
signup_form_template = "lms.plugins.show_custom_signup"
|
signup_form_template = "lms.plugins.show_custom_signup"
|
||||||
|
|
||||||
on_session_creation = "lms.overrides.user.on_session_creation"
|
|
||||||
|
|
||||||
add_to_apps_screen = [
|
add_to_apps_screen = [
|
||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
|
|||||||
129
lms/lms/api.py
129
lms/lms/api.py
@@ -7,15 +7,10 @@ import zipfile
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import requests
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from frappe.translate import get_all_translations
|
from frappe.translate import get_all_translations
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import DocType
|
|
||||||
from frappe.query_builder.functions import Count
|
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
time_diff,
|
|
||||||
now_datetime,
|
|
||||||
get_datetime,
|
get_datetime,
|
||||||
cint,
|
cint,
|
||||||
flt,
|
flt,
|
||||||
@@ -24,10 +19,13 @@ from frappe.utils import (
|
|||||||
format_date,
|
format_date,
|
||||||
date_diff,
|
date_diff,
|
||||||
)
|
)
|
||||||
from typing import Optional
|
|
||||||
from lms.lms.utils import get_average_rating, get_lesson_count
|
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||||
|
from frappe.integrations.frappe_providers.frappecloud_billing import (
|
||||||
|
is_fc_site,
|
||||||
|
current_site_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -180,6 +178,11 @@ def get_user_info():
|
|||||||
user.is_moderator = "Moderator" in user.roles
|
user.is_moderator = "Moderator" in user.roles
|
||||||
user.is_evaluator = "Batch Evaluator" in user.roles
|
user.is_evaluator = "Batch Evaluator" in user.roles
|
||||||
user.is_student = "LMS Student" in user.roles
|
user.is_student = "LMS Student" in user.roles
|
||||||
|
user.is_fc_site = is_fc_site()
|
||||||
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -193,24 +196,24 @@ def get_translations():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def validate_billing_access(type, name):
|
def validate_billing_access(billing_type, name):
|
||||||
access = True
|
access = True
|
||||||
message = ""
|
message = ""
|
||||||
doctype = "LMS Course" if type == "course" else "LMS Batch"
|
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
|
||||||
|
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
access = False
|
access = False
|
||||||
message = _("Please login to continue with payment.")
|
message = _("Please login to continue with payment.")
|
||||||
|
|
||||||
if type not in ["course", "batch"]:
|
if access and billing_type not in ["course", "batch", "certificate"]:
|
||||||
access = False
|
access = False
|
||||||
message = _("Module is incorrect.")
|
message = _("Module is incorrect.")
|
||||||
|
|
||||||
if not frappe.db.exists(doctype, name):
|
if access and not frappe.db.exists(doctype, name):
|
||||||
access = False
|
access = False
|
||||||
message = _("Module Name is incorrect or does not exist.")
|
message = _("Module Name is incorrect or does not exist.")
|
||||||
|
|
||||||
if type == "course":
|
if access and billing_type == "course":
|
||||||
membership = frappe.db.exists(
|
membership = frappe.db.exists(
|
||||||
"LMS Enrollment", {"member": frappe.session.user, "course": name}
|
"LMS Enrollment", {"member": frappe.session.user, "course": name}
|
||||||
)
|
)
|
||||||
@@ -218,7 +221,7 @@ def validate_billing_access(type, name):
|
|||||||
access = False
|
access = False
|
||||||
message = _("You are already enrolled for this course.")
|
message = _("You are already enrolled for this course.")
|
||||||
|
|
||||||
else:
|
elif access and billing_type == "batch":
|
||||||
membership = frappe.db.exists(
|
membership = frappe.db.exists(
|
||||||
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
|
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
|
||||||
)
|
)
|
||||||
@@ -226,6 +229,19 @@ def validate_billing_access(type, name):
|
|||||||
access = False
|
access = False
|
||||||
message = _("You are already enrolled for this batch.")
|
message = _("You are already enrolled for this batch.")
|
||||||
|
|
||||||
|
elif access and billing_type == "certificate":
|
||||||
|
purchased_certificate = frappe.db.exists(
|
||||||
|
"LMS Enrollment",
|
||||||
|
{
|
||||||
|
"course": name,
|
||||||
|
"member": frappe.session.user,
|
||||||
|
"purchased_certificate": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if purchased_certificate:
|
||||||
|
access = False
|
||||||
|
message = _("You have already purchased the certificate for this course.")
|
||||||
|
|
||||||
address = frappe.db.get_value(
|
address = frappe.db.get_value(
|
||||||
"Address",
|
"Address",
|
||||||
{"email_id": frappe.session.user},
|
{"email_id": frappe.session.user},
|
||||||
@@ -373,7 +389,7 @@ def get_evaluator_details(evaluator):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_certified_participants(filters=None, start=0, page_length=30, search=None):
|
def get_certified_participants(filters=None, start=0, page_length=30):
|
||||||
or_filters = {}
|
or_filters = {}
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -845,7 +861,7 @@ def update_course_statistics():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_announcements(batch):
|
def get_announcements(batch):
|
||||||
return frappe.get_all(
|
communications = frappe.get_all(
|
||||||
"Communication",
|
"Communication",
|
||||||
filters={
|
filters={
|
||||||
"reference_doctype": "LMS Batch",
|
"reference_doctype": "LMS Batch",
|
||||||
@@ -863,6 +879,13 @@ def get_announcements(batch):
|
|||||||
order_by="communication_date desc",
|
order_by="communication_date desc",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for communication in communications:
|
||||||
|
communication.image = frappe.get_cached_value(
|
||||||
|
"User", communication.sender, "user_image"
|
||||||
|
)
|
||||||
|
|
||||||
|
return communications
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def delete_course(course):
|
def delete_course(course):
|
||||||
@@ -1228,13 +1251,73 @@ def is_guest_allowed():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_announcement(students, cc, subject, content):
|
def cancel_evaluation(evaluation):
|
||||||
for student in students:
|
evaluation = frappe._dict(evaluation)
|
||||||
frappe.sendmail(
|
|
||||||
recipients=student,
|
if evaluation.member != frappe.session.user:
|
||||||
cc=cc,
|
return
|
||||||
subject=subject,
|
|
||||||
message=content,
|
frappe.db.set_value("LMS Certificate Request", evaluation.name, "status", "Cancelled")
|
||||||
header=[subject, "green"],
|
events = frappe.get_all(
|
||||||
retry=3,
|
"Event Participants",
|
||||||
|
{
|
||||||
|
"email": evaluation.member,
|
||||||
|
},
|
||||||
|
["parent", "name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
info = frappe.db.get_value("Event", event.parent, ["starts_on", "subject"], as_dict=1)
|
||||||
|
date = str(info.starts_on).split(" ")[0]
|
||||||
|
|
||||||
|
if (
|
||||||
|
date == str(evaluation.date.format("YYYY-MM-DD"))
|
||||||
|
and evaluation.member_name in info.subject
|
||||||
|
):
|
||||||
|
communication = frappe.db.get_value(
|
||||||
|
"Communication",
|
||||||
|
{"reference_doctype": "Event", "reference_name": event.parent},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
if communication:
|
||||||
|
frappe.delete_doc("Communication", communication, ignore_permissions=True)
|
||||||
|
|
||||||
|
frappe.delete_doc("Event Participants", event.name, ignore_permissions=True)
|
||||||
|
frappe.delete_doc("Event", event.parent, ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_certification_details(course):
|
||||||
|
membership = None
|
||||||
|
filters = {"course": course, "member": frappe.session.user}
|
||||||
|
|
||||||
|
if frappe.db.exists("LMS Enrollment", filters):
|
||||||
|
membership = frappe.db.get_value(
|
||||||
|
"LMS Enrollment",
|
||||||
|
filters,
|
||||||
|
["name", "certificate", "purchased_certificate"],
|
||||||
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
paid_certificate = frappe.db.get_value("LMS Course", course, "paid_certificate")
|
||||||
|
|
||||||
|
return {"membership": membership, "paid_certificate": paid_certificate}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def save_role(user, role, value):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
|
if cint(value):
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Has Role",
|
||||||
|
"parent": user,
|
||||||
|
"role": role,
|
||||||
|
"parenttype": "User",
|
||||||
|
"parentfield": "roles",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.save(ignore_permissions=True)
|
||||||
|
else:
|
||||||
|
frappe.db.delete("Has Role", {"parent": user, "role": role})
|
||||||
|
return True
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-15 18:45:08.614466",
|
"modified": "2025-02-24 12:17:08.436659",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Evaluator",
|
"name": "Course Evaluator",
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"evaluator",
|
||||||
"section_break_dlzh",
|
"section_break_dlzh",
|
||||||
"assignment_attachment",
|
"assignment_attachment",
|
||||||
"answer",
|
"answer",
|
||||||
|
"section_break_ydgh",
|
||||||
"column_break_oqqy",
|
"column_break_oqqy",
|
||||||
"evaluator",
|
|
||||||
"status",
|
"status",
|
||||||
"comments",
|
"comments",
|
||||||
"section_break_rqal",
|
"section_break_rqal",
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "comments",
|
"fieldname": "comments",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Text Editor",
|
||||||
"label": "Comments"
|
"label": "Comments"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,12 +140,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_oqqy",
|
"fieldname": "column_break_oqqy",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ydgh",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-12-24 21:22:35.212732",
|
"modified": "2025-02-17 18:40:53.374932",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Assignment Submission",
|
"name": "LMS Assignment Submission",
|
||||||
|
|||||||
@@ -8,25 +8,31 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"published",
|
"section_break_earo",
|
||||||
"title",
|
"title",
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"allow_self_enrollment",
|
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"timezone",
|
"timezone",
|
||||||
"section_break_rgfj",
|
"section_break_cssv",
|
||||||
"medium",
|
"published",
|
||||||
"category",
|
"column_break_wfkz",
|
||||||
"column_break_flwy",
|
"allow_self_enrollment",
|
||||||
"seat_count",
|
"column_break_vnrp",
|
||||||
"evaluation_end_date",
|
"certification",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"description",
|
"description",
|
||||||
"column_break_hlqw",
|
"column_break_hlqw",
|
||||||
"instructors",
|
"instructors",
|
||||||
|
"section_break_rgfj",
|
||||||
|
"medium",
|
||||||
|
"category",
|
||||||
|
"confirmation_email_template",
|
||||||
|
"column_break_flwy",
|
||||||
|
"seat_count",
|
||||||
|
"evaluation_end_date",
|
||||||
"meta_image",
|
"meta_image",
|
||||||
"section_break_khcn",
|
"section_break_khcn",
|
||||||
"batch_details",
|
"batch_details",
|
||||||
@@ -206,6 +212,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "published",
|
"fieldname": "published",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Published"
|
"label": "Published"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,6 +325,35 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_khcn",
|
"fieldname": "section_break_khcn",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "confirmation_email_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Confirmation Email Template",
|
||||||
|
"options": "Email Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wfkz",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vnrp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "certification",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Certification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_earo",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_cssv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -335,7 +371,7 @@
|
|||||||
"link_fieldname": "batch_name"
|
"link_fieldname": "batch_name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-02-12 11:59:35.312487",
|
"modified": "2025-02-18 15:43:18.512504",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import json
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, format_datetime, get_time
|
from frappe.utils import cint, format_datetime, get_time, add_days, nowdate
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
get_lessons,
|
|
||||||
get_lesson_index,
|
get_lesson_index,
|
||||||
get_lesson_url,
|
get_lesson_url,
|
||||||
get_quiz_details,
|
get_quiz_details,
|
||||||
@@ -176,6 +175,10 @@ def create_live_class(
|
|||||||
class_details = frappe.get_doc(payload)
|
class_details = frappe.get_doc(payload)
|
||||||
class_details.save()
|
class_details.save()
|
||||||
return class_details
|
return class_details
|
||||||
|
else:
|
||||||
|
frappe.throw(
|
||||||
|
_("Error creating live class. Please try again. {0}").format(response.text)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
def authenticate():
|
||||||
@@ -254,17 +257,6 @@ def create_batch(
|
|||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def fetch_lessons(courses):
|
|
||||||
lessons = []
|
|
||||||
courses = json.loads(courses)
|
|
||||||
|
|
||||||
for course in courses:
|
|
||||||
lessons.extend(get_lessons(course.get("course")))
|
|
||||||
|
|
||||||
return lessons
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_course(course, parent, name=None, evaluator=None):
|
def add_course(course, parent, name=None, evaluator=None):
|
||||||
frappe.only_for("Moderator")
|
frappe.only_for("Moderator")
|
||||||
@@ -405,3 +397,40 @@ def is_milestone_complete(idx, batch):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def send_batch_start_reminder():
|
||||||
|
batches = frappe.get_all(
|
||||||
|
"LMS Batch",
|
||||||
|
{"start_date": add_days(nowdate(), 1), "published": 1},
|
||||||
|
["name", "title", "start_date", "start_time", "medium"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
students = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment", {"batch": batch}, ["member", "member_name"]
|
||||||
|
)
|
||||||
|
for student in students:
|
||||||
|
send_mail(batch, student)
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(batch, student):
|
||||||
|
subject = _("Batch Start Reminder")
|
||||||
|
template = "batch_start_reminder"
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"student_name": student.member_name,
|
||||||
|
"title": batch.title,
|
||||||
|
"start_date": batch.start_date,
|
||||||
|
"start_time": batch.start_time,
|
||||||
|
"medium": batch.medium,
|
||||||
|
"name": batch.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=student.member,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
header=[_(f"Batch Start Reminder: {batch.title}"), "orange"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -76,17 +76,26 @@ def send_confirmation_email(doc):
|
|||||||
|
|
||||||
|
|
||||||
def send_mail(doc):
|
def send_mail(doc):
|
||||||
subject = _("Enrollment Confirmation for the Next Training Batch")
|
|
||||||
template = "batch_confirmation"
|
|
||||||
custom_template = frappe.db.get_single_value(
|
|
||||||
"LMS Settings", "batch_confirmation_template"
|
|
||||||
)
|
|
||||||
batch = frappe.db.get_value(
|
batch = frappe.db.get_value(
|
||||||
"LMS Batch",
|
"LMS Batch",
|
||||||
doc.batch,
|
doc.batch,
|
||||||
["name", "title", "start_date", "start_time", "medium"],
|
[
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"start_date",
|
||||||
|
"start_time",
|
||||||
|
"medium",
|
||||||
|
"confirmation_email_template",
|
||||||
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
subject = _("Enrollment Confirmation for {0}").format(batch.title)
|
||||||
|
template = "batch_confirmation"
|
||||||
|
custom_template = batch.confirmation_email_template or frappe.db.get_single_value(
|
||||||
|
"LMS Settings", "batch_confirmation_template"
|
||||||
|
)
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"title": batch.title,
|
"title": batch.title,
|
||||||
"student_name": doc.member_name,
|
"student_name": doc.member_name,
|
||||||
@@ -107,6 +116,6 @@ def send_mail(doc):
|
|||||||
template=template if not custom_template else None,
|
template=template if not custom_template else None,
|
||||||
content=content if custom_template else None,
|
content=content if custom_template else None,
|
||||||
args=args,
|
args=args,
|
||||||
header=[subject, "green"],
|
header=[_(batch.title), "green"],
|
||||||
retry=3,
|
retry=3,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-17 11:57:02.859109",
|
"modified": "2025-02-19 16:26:05.902473",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
@@ -156,6 +156,7 @@
|
|||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
|
|||||||
@@ -122,11 +122,6 @@ def create_certificate(course):
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
expires_after_yrs = int(frappe.db.get_value("LMS Course", course, "expiry"))
|
|
||||||
expiry_date = None
|
|
||||||
if expires_after_yrs:
|
|
||||||
expiry_date = add_years(nowdate(), expires_after_yrs)
|
|
||||||
|
|
||||||
default_certificate_template = frappe.db.get_value(
|
default_certificate_template = frappe.db.get_value(
|
||||||
"Property Setter",
|
"Property Setter",
|
||||||
{
|
{
|
||||||
@@ -148,7 +143,6 @@ def create_certificate(course):
|
|||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
"course": course,
|
"course": course,
|
||||||
"issue_date": nowdate(),
|
"issue_date": nowdate(),
|
||||||
"expiry_date": expiry_date,
|
|
||||||
"template": default_certificate_template,
|
"template": default_certificate_template,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class TestLMSCertificate(unittest.TestCase):
|
|||||||
"Test Certificate",
|
"Test Certificate",
|
||||||
{
|
{
|
||||||
"enable_certification": 1,
|
"enable_certification": 1,
|
||||||
"expiry": 2,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
certificate = create_certificate(course.name)
|
certificate = create_certificate(course.name)
|
||||||
@@ -24,7 +23,6 @@ class TestLMSCertificate(unittest.TestCase):
|
|||||||
self.assertEqual(certificate.member, "Administrator")
|
self.assertEqual(certificate.member, "Administrator")
|
||||||
self.assertEqual(certificate.course, course.name)
|
self.assertEqual(certificate.course, course.name)
|
||||||
self.assertEqual(certificate.issue_date, nowdate())
|
self.assertEqual(certificate.issue_date, nowdate())
|
||||||
self.assertEqual(certificate.expiry_date, add_years(nowdate(), cint(course.expiry)))
|
|
||||||
|
|
||||||
frappe.db.delete("LMS Certificate", certificate.name)
|
frappe.db.delete("LMS Certificate", certificate.name)
|
||||||
frappe.db.delete("LMS Course", course.name)
|
frappe.db.delete("LMS Course", course.name)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
frappe.ui.form.on("LMS Certificate Evaluation", {
|
frappe.ui.form.on("LMS Certificate Evaluation", {
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
if (!frm.is_new() && frm.doc.status == "Pass") {
|
if (!frm.is_new() && frm.doc.status == "Pass") {
|
||||||
frm.add_custom_button(__("Create LMS Certificate"), () => {
|
frm.add_custom_button(__("Create Certificate"), () => {
|
||||||
frappe.model.open_mapped_doc({
|
frappe.model.open_mapped_doc({
|
||||||
method: "lms.lms.doctype.lms_certificate_evaluation.lms_certificate_evaluation.create_lms_certificate",
|
method: "lms.lms.doctype.lms_certificate_evaluation.lms_certificate_evaluation.create_lms_certificate",
|
||||||
frm: frm,
|
frm: frm,
|
||||||
|
|||||||
@@ -3,18 +3,15 @@
|
|||||||
|
|
||||||
frappe.ui.form.on("LMS Certificate Request", {
|
frappe.ui.form.on("LMS Certificate Request", {
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
if (!frm.is_new()) {
|
if (!frm.is_new() && frm.doc.status == "Upcoming") {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(__("Conduct Evaluation"), () => {
|
||||||
__("Create LMS Certificate Evaluation"),
|
frappe.model.open_mapped_doc({
|
||||||
() => {
|
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_lms_certificate_evaluation",
|
||||||
frappe.model.open_mapped_doc({
|
frm: frm,
|
||||||
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_lms_certificate_evaluation",
|
});
|
||||||
frm: frm,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!frm.doc.google_meet_link) {
|
if (!frm.doc.google_meet_link && frm.doc.status == "Upcoming") {
|
||||||
frm.add_custom_button(__("Generate Google Meet Link"), () => {
|
frm.add_custom_button(__("Generate Google Meet Link"), () => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.setup_calendar_event",
|
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.setup_calendar_event",
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"google_meet_link",
|
"google_meet_link",
|
||||||
"column_break_ddyh",
|
"column_break_ddyh",
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time"
|
"end_time",
|
||||||
|
"status"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -144,11 +145,19 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Batch Title"
|
"label": "Batch Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Upcoming\nCompleted\nCancelled",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-09-11 11:19:44.669132",
|
"modified": "2025-02-19 17:20:02.526294",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
@@ -204,6 +213,19 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [
|
||||||
|
{
|
||||||
|
"color": "Blue",
|
||||||
|
"title": "Upcoming"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Green",
|
||||||
|
"title": "Completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Red",
|
||||||
|
"title": "Cancelled"
|
||||||
|
}
|
||||||
|
],
|
||||||
"title_field": "member_name"
|
"title_field": "member_name"
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,7 @@ class LMSCertificateRequest(Document):
|
|||||||
"member": self.member,
|
"member": self.member,
|
||||||
"course": self.course,
|
"course": self.course,
|
||||||
"name": ["!=", self.name],
|
"name": ["!=", self.name],
|
||||||
|
"status": "Upcoming",
|
||||||
},
|
},
|
||||||
["date", "start_time", "course"],
|
["date", "start_time", "course"],
|
||||||
)
|
)
|
||||||
@@ -150,7 +151,11 @@ def schedule_evals():
|
|||||||
timelapse = add_to_date(get_datetime(), hours=-5)
|
timelapse = add_to_date(get_datetime(), hours=-5)
|
||||||
evals = frappe.get_all(
|
evals = frappe.get_all(
|
||||||
"LMS Certificate Request",
|
"LMS Certificate Request",
|
||||||
{"creation": [">=", timelapse], "google_meet_link": ["is", "not set"]},
|
{
|
||||||
|
"creation": [">=", timelapse],
|
||||||
|
"google_meet_link": ["is", "not set"],
|
||||||
|
"status": "Upcoming",
|
||||||
|
},
|
||||||
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
|
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
|
||||||
)
|
)
|
||||||
for eval in evals:
|
for eval in evals:
|
||||||
@@ -254,3 +259,20 @@ def create_lms_certificate_evaluation(source_name, target_doc=None):
|
|||||||
target_doc,
|
target_doc,
|
||||||
)
|
)
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def mark_eval_as_completed():
|
||||||
|
requests = frappe.get_all(
|
||||||
|
"LMS Certificate Request",
|
||||||
|
{
|
||||||
|
"status": "Upcoming",
|
||||||
|
"date": ["<=", getdate()],
|
||||||
|
},
|
||||||
|
["name", "end_time", "date"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for req in requests:
|
||||||
|
if req.date < getdate():
|
||||||
|
frappe.db.set_value("LMS Certificate Request", req.name, "status", "Completed")
|
||||||
|
elif req.date == getdate() and get_time(req.end_time) < get_time(nowtime()):
|
||||||
|
frappe.db.set_value("LMS Certificate Request", req.name, "status", "Completed")
|
||||||
|
|||||||
@@ -40,15 +40,13 @@
|
|||||||
"pricing_tab",
|
"pricing_tab",
|
||||||
"pricing_section",
|
"pricing_section",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
|
"enable_certification",
|
||||||
|
"paid_certificate",
|
||||||
|
"evaluator",
|
||||||
"column_break_acoj",
|
"column_break_acoj",
|
||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
"certification_tab",
|
|
||||||
"certification_section",
|
|
||||||
"enable_certification",
|
|
||||||
"column_break_rxww",
|
|
||||||
"expiry",
|
|
||||||
"tab_4_tab",
|
"tab_4_tab",
|
||||||
"statistics_section",
|
"statistics_section",
|
||||||
"enrollments",
|
"enrollments",
|
||||||
@@ -134,22 +132,11 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Course Settings"
|
"label": "Course Settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "certification_section",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "enable_certification",
|
"fieldname": "enable_certification",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Certification"
|
"label": "Completion Certificate"
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "enable_certification",
|
|
||||||
"fieldname": "expiry",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Certification Expires After (Years)"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "related_courses",
|
"fieldname": "related_courses",
|
||||||
@@ -181,7 +168,6 @@
|
|||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
@@ -195,22 +181,16 @@
|
|||||||
"label": "Paid Course"
|
"label": "Paid Course"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
|
||||||
"fieldname": "course_price",
|
"fieldname": "course_price",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Course Price",
|
"label": "Amount",
|
||||||
"mandatory_depends_on": "paid_course"
|
"mandatory_depends_on": "paid_course"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_rxww",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_acoj",
|
"fieldname": "column_break_acoj",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
|
||||||
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
||||||
"fieldname": "amount_usd",
|
"fieldname": "amount_usd",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
@@ -238,12 +218,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "pricing_tab",
|
"fieldname": "pricing_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Pricing"
|
"label": "Pricing and Certification"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "certification_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Certification"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_htgn",
|
"fieldname": "column_break_htgn",
|
||||||
@@ -267,14 +242,14 @@
|
|||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "enrollments",
|
"fieldname": "enrollments",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Int",
|
||||||
"label": "Enrollments",
|
"label": "Enrollments",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "lessons",
|
"fieldname": "lessons",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Int",
|
||||||
"label": "Lessons",
|
"label": "Lessons",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -284,6 +259,19 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Rating",
|
"label": "Rating",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "paid_certificate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Paid Certificate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "paid_certificate",
|
||||||
|
"fieldname": "evaluator",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Evaluator",
|
||||||
|
"options": "Course Evaluator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -310,7 +298,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-10-30 23:08:31.842860",
|
"modified": "2025-03-04 15:43:25.151554",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import json
|
|||||||
import random
|
import random
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, today
|
from frappe.utils import today, cint
|
||||||
from frappe.utils.telemetry import capture
|
from lms.lms.utils import get_chapters
|
||||||
from lms.lms.utils import get_chapters, can_create_courses
|
|
||||||
from ...utils import generate_slug, validate_image, update_payment_record
|
from ...utils import generate_slug, validate_image, update_payment_record
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ class LMSCourse(Document):
|
|||||||
self.validate_video_link()
|
self.validate_video_link()
|
||||||
self.validate_status()
|
self.validate_status()
|
||||||
self.validate_payments_app()
|
self.validate_payments_app()
|
||||||
|
self.validate_certification()
|
||||||
self.validate_amount_and_currency()
|
self.validate_amount_and_currency()
|
||||||
self.image = validate_image(self.image)
|
self.image = validate_image(self.image)
|
||||||
|
|
||||||
@@ -52,10 +52,22 @@ class LMSCourse(Document):
|
|||||||
if "payments" not in installed_apps:
|
if "payments" not in installed_apps:
|
||||||
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
||||||
|
|
||||||
|
def validate_certification(self):
|
||||||
|
if self.enable_certification and self.paid_certificate:
|
||||||
|
frappe.throw(
|
||||||
|
_("A course cannot have both paid certificate and certificate of completion.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.paid_certificate and not self.evaluator:
|
||||||
|
frappe.throw(_("Evaluator is required for paid certificates."))
|
||||||
|
|
||||||
def validate_amount_and_currency(self):
|
def validate_amount_and_currency(self):
|
||||||
if self.paid_course and (not self.course_price and not self.currency):
|
if self.paid_course and (cint(self.course_price) < 0 or not self.currency):
|
||||||
frappe.throw(_("Amount and currency are required for paid courses."))
|
frappe.throw(_("Amount and currency are required for paid courses."))
|
||||||
|
|
||||||
|
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
|
||||||
|
frappe.throw(_("Amount and currency are required for paid certificates."))
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||||
self.send_email_to_interested_users()
|
self.send_email_to_interested_users()
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"member_username",
|
"member_username",
|
||||||
|
"certification_section",
|
||||||
|
"purchased_certificate",
|
||||||
|
"certificate",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"cohort",
|
"cohort",
|
||||||
"subgroup",
|
"subgroup",
|
||||||
@@ -123,11 +126,28 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Payment",
|
"label": "Payment",
|
||||||
"options": "LMS Payment"
|
"options": "LMS Payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certification_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Certification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "purchased_certificate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Purchased Certificate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certificate",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Certificate",
|
||||||
|
"options": "LMS Certificate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-10-30 12:44:16.103598",
|
"modified": "2025-02-21 17:11:37.986157",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Enrollment",
|
"name": "LMS Enrollment",
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.utils import cint, get_datetime
|
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
||||||
|
|
||||||
|
|
||||||
class LMSLiveClass(Document):
|
class LMSLiveClass(Document):
|
||||||
@@ -56,8 +57,48 @@ class LMSLiveClass(Document):
|
|||||||
{
|
{
|
||||||
"sync_with_google_calendar": 1,
|
"sync_with_google_calendar": 1,
|
||||||
"google_calendar": calendar,
|
"google_calendar": calendar,
|
||||||
"description": f"A Live Class has been scheduled on {frappe.utils.format_date(self.date, 'medium')} at { frappe.utils.format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
|
"description": f"A Live Class has been scheduled on {format_date(self.date, 'medium')} at {format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
|
def send_live_class_reminder():
|
||||||
|
classes = frappe.get_all(
|
||||||
|
"LMS Live Class",
|
||||||
|
{
|
||||||
|
"date": nowdate(),
|
||||||
|
},
|
||||||
|
["name", "batch_name", "title", "date", "time"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for live_class in classes:
|
||||||
|
students = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment",
|
||||||
|
{"batch": live_class.batch_name},
|
||||||
|
["member", "member_name"],
|
||||||
|
)
|
||||||
|
for student in students:
|
||||||
|
send_mail(live_class, student)
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(live_class, student):
|
||||||
|
subject = f"Your class on {live_class.title} is tomorrow"
|
||||||
|
template = "live_class_reminder"
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"student_name": student.member_name,
|
||||||
|
"title": live_class.title,
|
||||||
|
"date": live_class.date,
|
||||||
|
"time": live_class.time,
|
||||||
|
"batch_name": live_class.batch_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=student.member,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"payment_for_document_type",
|
"payment_for_document_type",
|
||||||
"payment_for_document",
|
"payment_for_document",
|
||||||
"payment_received",
|
"payment_received",
|
||||||
|
"payment_for_certificate",
|
||||||
"payment_details_section",
|
"payment_details_section",
|
||||||
"currency",
|
"currency",
|
||||||
"amount",
|
"amount",
|
||||||
@@ -136,11 +137,26 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Source",
|
"label": "Source",
|
||||||
"options": "LMS Source"
|
"options": "LMS Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "payment_for_certificate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Payment for Certificate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2025-02-11 14:48:27.801895",
|
{
|
||||||
|
"link_doctype": "LMS Batch Enrollment",
|
||||||
|
"link_fieldname": "payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link_doctype": "LMS Enrollment",
|
||||||
|
"link_fieldname": "payment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2025-02-21 18:29:55.436611",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Payment",
|
"name": "LMS Payment",
|
||||||
|
|||||||
@@ -33,9 +33,24 @@ def send_payment_reminder():
|
|||||||
)
|
)
|
||||||
|
|
||||||
for payment in incomplete_payments:
|
for payment in incomplete_payments:
|
||||||
|
if has_paid_later(payment):
|
||||||
|
continue
|
||||||
|
|
||||||
send_mail(payment)
|
send_mail(payment)
|
||||||
|
|
||||||
|
|
||||||
|
def has_paid_later(payment):
|
||||||
|
return frappe.db.exists(
|
||||||
|
"LMS Payment",
|
||||||
|
{
|
||||||
|
"member": payment.member,
|
||||||
|
"payment_received": 1,
|
||||||
|
"payment_for_document": payment.payment_for_document,
|
||||||
|
"payment_for_document_type": payment.payment_for_document_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_mail(payment):
|
def send_mail(payment):
|
||||||
subject = _("Complete Your Enrollment - Don't miss out!")
|
subject = _("Complete Your Enrollment - Don't miss out!")
|
||||||
template = "payment_reminder"
|
template = "payment_reminder"
|
||||||
|
|||||||
@@ -10,12 +10,29 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
|
|||||||
|
|
||||||
class LMSQuizSubmission(Document):
|
class LMSQuizSubmission(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_if_max_attempts_exceeded()
|
||||||
self.validate_marks()
|
self.validate_marks()
|
||||||
self.set_percentage()
|
self.set_percentage()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.notify_member()
|
self.notify_member()
|
||||||
|
|
||||||
|
def validate_if_max_attempts_exceeded(self):
|
||||||
|
max_attempts = frappe.db.get_value("LMS Quiz", self.quiz, ["max_attempts"])
|
||||||
|
if max_attempts == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_user_submission_count = frappe.db.count(
|
||||||
|
self.doctype, filters={"quiz": self.quiz, "member": frappe.session.user}
|
||||||
|
)
|
||||||
|
if current_user_submission_count >= max_attempts:
|
||||||
|
frappe.throw(
|
||||||
|
_("You have exceeded the maximum number of attempts ({0}) for this quiz").format(
|
||||||
|
max_attempts
|
||||||
|
),
|
||||||
|
MaximumAttemptsExceededError,
|
||||||
|
)
|
||||||
|
|
||||||
def validate_marks(self):
|
def validate_marks(self):
|
||||||
self.score = 0
|
self.score = 0
|
||||||
for row in self.result:
|
for row in self.result:
|
||||||
@@ -52,3 +69,7 @@ class LMSQuizSubmission(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
make_notification_logs(notification, [self.member])
|
make_notification_logs(notification, [self.member])
|
||||||
|
|
||||||
|
|
||||||
|
class MaximumAttemptsExceededError(frappe.DuplicateEntryError):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -18,19 +18,26 @@ def validate_currency(payment_gateway, currency):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_link(doctype, docname, title, amount, total_amount, currency, address):
|
def get_payment_link(
|
||||||
|
doctype,
|
||||||
|
docname,
|
||||||
|
title,
|
||||||
|
amount,
|
||||||
|
total_amount,
|
||||||
|
currency,
|
||||||
|
address,
|
||||||
|
redirect_to,
|
||||||
|
payment_for_certificate,
|
||||||
|
):
|
||||||
payment_gateway = get_payment_gateway()
|
payment_gateway = get_payment_gateway()
|
||||||
address = frappe._dict(address)
|
address = frappe._dict(address)
|
||||||
amount_with_gst = total_amount if total_amount != amount else 0
|
amount_with_gst = total_amount if total_amount != amount else 0
|
||||||
|
|
||||||
payment = record_payment(address, doctype, docname, amount, currency, amount_with_gst)
|
payment = record_payment(
|
||||||
|
address, doctype, docname, amount, currency, amount_with_gst, payment_for_certificate
|
||||||
|
)
|
||||||
controller = get_controller(payment_gateway)
|
controller = get_controller(payment_gateway)
|
||||||
|
|
||||||
if doctype == "LMS Course":
|
|
||||||
redirect_to = f"/lms/courses/{docname}/learn/1-1"
|
|
||||||
elif doctype == "LMS Batch":
|
|
||||||
redirect_to = f"/lms/batches/{docname}"
|
|
||||||
|
|
||||||
payment_details = {
|
payment_details = {
|
||||||
"amount": total_amount,
|
"amount": total_amount,
|
||||||
"title": f"Payment for {doctype} {title} {docname}",
|
"title": f"Payment for {doctype} {title} {docname}",
|
||||||
@@ -53,7 +60,15 @@ def get_payment_link(doctype, docname, title, amount, total_amount, currency, ad
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def record_payment(address, doctype, docname, amount, currency, amount_with_gst=0):
|
def record_payment(
|
||||||
|
address,
|
||||||
|
doctype,
|
||||||
|
docname,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
amount_with_gst=0,
|
||||||
|
payment_for_certificate=0,
|
||||||
|
):
|
||||||
address = frappe._dict(address)
|
address = frappe._dict(address)
|
||||||
address_name = save_address(address)
|
address_name = save_address(address)
|
||||||
|
|
||||||
@@ -71,6 +86,7 @@ def record_payment(address, doctype, docname, amount, currency, amount_with_gst=
|
|||||||
"source": address.source,
|
"source": address.source,
|
||||||
"payment_for_document_type": doctype,
|
"payment_for_document_type": doctype,
|
||||||
"payment_for_document": docname,
|
"payment_for_document": docname,
|
||||||
|
"payment_for_certificate": payment_for_certificate,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
payment_doc.save(ignore_permissions=True)
|
payment_doc.save(ignore_permissions=True)
|
||||||
|
|||||||
85
lms/lms/user.py
Normal file
85
lms/lms/user.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.naming import append_number_if_name_exists
|
||||||
|
from frappe.website.utils import cleanup_page_name
|
||||||
|
from frappe.website.utils import is_signup_disabled
|
||||||
|
from frappe.utils import random_string, escape_html
|
||||||
|
from lms.lms.utils import get_country_code
|
||||||
|
|
||||||
|
|
||||||
|
def validate_username_duplicates(doc, method):
|
||||||
|
while not doc.username or doc.username_exists():
|
||||||
|
doc.username = append_number_if_name_exists(
|
||||||
|
doc.doctype, cleanup_page_name(doc.full_name), fieldname="username"
|
||||||
|
)
|
||||||
|
if " " in doc.username:
|
||||||
|
doc.username = doc.username.replace(" ", "")
|
||||||
|
|
||||||
|
if len(doc.username) < 4:
|
||||||
|
doc.username = doc.email.replace("@", "").replace(".", "")
|
||||||
|
|
||||||
|
|
||||||
|
def after_insert(doc, method):
|
||||||
|
doc.add_roles("LMS Student")
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def sign_up(email, full_name, verify_terms, user_category):
|
||||||
|
if is_signup_disabled():
|
||||||
|
frappe.throw(_("Sign Up is disabled"), _("Not Allowed"))
|
||||||
|
|
||||||
|
user = frappe.db.get("User", {"email": email})
|
||||||
|
if user:
|
||||||
|
if user.enabled:
|
||||||
|
return 0, _("Already Registered")
|
||||||
|
else:
|
||||||
|
return 0, _("Registered but disabled")
|
||||||
|
else:
|
||||||
|
if frappe.db.get_creation_count("User", 60) > 300:
|
||||||
|
frappe.respond_as_web_page(
|
||||||
|
_("Temporarily Disabled"),
|
||||||
|
_(
|
||||||
|
"Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
||||||
|
),
|
||||||
|
http_status_code=429,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "User",
|
||||||
|
"email": email,
|
||||||
|
"first_name": escape_html(full_name),
|
||||||
|
"verify_terms": verify_terms,
|
||||||
|
"user_category": user_category,
|
||||||
|
"country": "",
|
||||||
|
"enabled": 1,
|
||||||
|
"new_password": random_string(10),
|
||||||
|
"user_type": "Website User",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user.flags.ignore_permissions = True
|
||||||
|
user.flags.ignore_password_policy = True
|
||||||
|
user.insert()
|
||||||
|
|
||||||
|
# set default signup role as per Portal Settings
|
||||||
|
default_role = frappe.db.get_single_value("Portal Settings", "default_role")
|
||||||
|
if default_role:
|
||||||
|
user.add_roles(default_role)
|
||||||
|
|
||||||
|
user.add_roles("LMS Student")
|
||||||
|
set_country_from_ip(None, user.name)
|
||||||
|
|
||||||
|
if user.flags.email_sent:
|
||||||
|
return 1, _("Please check your email for verification")
|
||||||
|
else:
|
||||||
|
return 2, _("Please ask your administrator to verify your sign-up")
|
||||||
|
|
||||||
|
|
||||||
|
def set_country_from_ip(login_manager=None, user=None):
|
||||||
|
if not user and login_manager:
|
||||||
|
user = login_manager.user
|
||||||
|
user_country = frappe.db.get_value("User", user, "country")
|
||||||
|
# if user_country:
|
||||||
|
# return
|
||||||
|
frappe.db.set_value("User", user, "country", get_country_code())
|
||||||
|
return
|
||||||
307
lms/lms/utils.py
307
lms/lms/utils.py
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import frappe
|
import frappe
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import razorpay
|
import razorpay
|
||||||
import requests
|
import requests
|
||||||
@@ -68,27 +69,26 @@ def generate_slug(title, doctype):
|
|||||||
return slugify(title, used_slugs=slugs)
|
return slugify(title, used_slugs=slugs)
|
||||||
|
|
||||||
|
|
||||||
def get_membership(course, member=None, batch=None):
|
def get_membership(course, member=None):
|
||||||
if not member:
|
if not member:
|
||||||
member = frappe.session.user
|
member = frappe.session.user
|
||||||
|
|
||||||
filters = {"member": member, "course": course}
|
filters = {"member": member, "course": course}
|
||||||
if batch:
|
|
||||||
filters["batch_old"] = batch
|
|
||||||
|
|
||||||
is_member = frappe.db.exists("LMS Enrollment", filters)
|
if frappe.db.exists("LMS Enrollment", filters):
|
||||||
if is_member:
|
|
||||||
membership = frappe.db.get_value(
|
membership = frappe.db.get_value(
|
||||||
"LMS Enrollment",
|
"LMS Enrollment",
|
||||||
filters,
|
filters,
|
||||||
["name", "batch_old", "current_lesson", "member_type", "progress", "member"],
|
[
|
||||||
|
"name",
|
||||||
|
"current_lesson",
|
||||||
|
"progress",
|
||||||
|
"member",
|
||||||
|
"purchased_certificate",
|
||||||
|
"certificate",
|
||||||
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if membership and membership.batch_old:
|
|
||||||
membership.batch_title = frappe.db.get_value(
|
|
||||||
"LMS Batch Old", membership.batch_old, "title"
|
|
||||||
)
|
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -856,26 +856,44 @@ def is_onboarding_complete():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_evaluator(course, batch):
|
def get_evaluator(course, batch=None):
|
||||||
evaluator = None
|
evaluator = None
|
||||||
evaluator = frappe.db.get_value(
|
if batch:
|
||||||
"Batch Course",
|
evaluator = frappe.db.get_value(
|
||||||
{"parent": batch, "course": course},
|
"Batch Course",
|
||||||
"evaluator",
|
{"parent": batch, "course": course},
|
||||||
)
|
"evaluator",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
||||||
return evaluator
|
return evaluator
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_upcoming_evals(student, courses):
|
def get_upcoming_evals(student, courses, batch=None):
|
||||||
|
filters = {
|
||||||
|
"member": student,
|
||||||
|
"course": ["in", courses],
|
||||||
|
"date": [">=", frappe.utils.nowdate()],
|
||||||
|
"status": "Upcoming",
|
||||||
|
}
|
||||||
|
|
||||||
|
if batch:
|
||||||
|
filters["batch_name"] = batch
|
||||||
|
|
||||||
upcoming_evals = frappe.get_all(
|
upcoming_evals = frappe.get_all(
|
||||||
"LMS Certificate Request",
|
"LMS Certificate Request",
|
||||||
{
|
filters,
|
||||||
"member": student,
|
[
|
||||||
"course": ["in", courses],
|
"name",
|
||||||
"date": [">=", frappe.utils.nowdate()],
|
"date",
|
||||||
},
|
"start_time",
|
||||||
["date", "start_time", "course", "evaluator", "google_meet_link"],
|
"course",
|
||||||
|
"evaluator",
|
||||||
|
"google_meet_link",
|
||||||
|
"member",
|
||||||
|
"member_name",
|
||||||
|
],
|
||||||
order_by="date",
|
order_by="date",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -967,17 +985,145 @@ def change_currency(amount, currency, country=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_courses():
|
def get_courses(filters=None, start=0, page_length=20):
|
||||||
"""Returns the list of courses."""
|
"""Returns the list of courses."""
|
||||||
courses = []
|
|
||||||
course_list = frappe.get_all("LMS Course", pluck="name")
|
|
||||||
for course in course_list:
|
|
||||||
courses.append(get_course_details(course))
|
|
||||||
|
|
||||||
courses = get_categorized_courses(courses)
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
filters, or_filters, show_featured = update_course_filters(filters)
|
||||||
|
fields = get_course_fields()
|
||||||
|
|
||||||
|
courses = frappe.get_all(
|
||||||
|
"LMS Course",
|
||||||
|
filters=filters,
|
||||||
|
fields=fields,
|
||||||
|
or_filters=or_filters,
|
||||||
|
order_by="enrollments desc",
|
||||||
|
start=start,
|
||||||
|
page_length=page_length,
|
||||||
|
)
|
||||||
|
if show_featured:
|
||||||
|
courses = get_featured_courses(filters, or_filters, fields) + courses
|
||||||
|
|
||||||
|
courses = get_enrollment_details(courses)
|
||||||
|
courses = get_course_card_details(courses)
|
||||||
return courses
|
return courses
|
||||||
|
|
||||||
|
|
||||||
|
def get_course_card_details(courses):
|
||||||
|
for course in courses:
|
||||||
|
course.instructors = get_instructors(course.name)
|
||||||
|
|
||||||
|
if course.paid_course and course.published == 1:
|
||||||
|
course.amount, course.currency = check_multicurrency(
|
||||||
|
course.course_price, course.currency, None, course.amount_usd
|
||||||
|
)
|
||||||
|
course.price = fmt_money(course.amount, 0, course.currency)
|
||||||
|
|
||||||
|
return courses
|
||||||
|
|
||||||
|
|
||||||
|
def get_course_or_filters(filters):
|
||||||
|
or_filters = {}
|
||||||
|
or_filters.update({"title": filters.get("title")})
|
||||||
|
or_filters.update({"short_introduction": filters.get("title")})
|
||||||
|
or_filters.update({"description": filters.get("title")})
|
||||||
|
or_filters.update({"tags": filters.get("title")})
|
||||||
|
return or_filters
|
||||||
|
|
||||||
|
|
||||||
|
def update_course_filters(filters):
|
||||||
|
or_filters = {}
|
||||||
|
show_featured = False
|
||||||
|
|
||||||
|
if filters.get("title"):
|
||||||
|
or_filters = get_course_or_filters(filters)
|
||||||
|
del filters["title"]
|
||||||
|
|
||||||
|
if filters.get("enrolled"):
|
||||||
|
enrolled_courses = frappe.get_all(
|
||||||
|
"LMS Enrollment", {"member": frappe.session.user}, pluck="course"
|
||||||
|
)
|
||||||
|
filters.update({"name": ["in", enrolled_courses]})
|
||||||
|
del filters["enrolled"]
|
||||||
|
|
||||||
|
if filters.get("created"):
|
||||||
|
created_courses = frappe.get_all(
|
||||||
|
"Course Instructor", {"instructor": frappe.session.user}, pluck="parent"
|
||||||
|
)
|
||||||
|
filters.update({"name": ["in", created_courses]})
|
||||||
|
del filters["created"]
|
||||||
|
|
||||||
|
if filters.get("live"):
|
||||||
|
filters.update({"featured": 0})
|
||||||
|
show_featured = True
|
||||||
|
del filters["live"]
|
||||||
|
|
||||||
|
if filters.get("certification"):
|
||||||
|
or_filters.update({"enable_certification": 1})
|
||||||
|
or_filters.update({"paid_certificate": 1})
|
||||||
|
del filters["certification"]
|
||||||
|
|
||||||
|
return filters, or_filters, show_featured
|
||||||
|
|
||||||
|
|
||||||
|
def get_enrollment_details(courses):
|
||||||
|
for course in courses:
|
||||||
|
filters = {
|
||||||
|
"course": course.name,
|
||||||
|
"member": frappe.session.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
if frappe.db.exists("LMS Enrollment", filters):
|
||||||
|
course.membership = frappe.db.get_value(
|
||||||
|
"LMS Enrollment",
|
||||||
|
filters,
|
||||||
|
["name", "course", "current_lesson", "progress", "member"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return courses
|
||||||
|
|
||||||
|
|
||||||
|
def get_featured_courses(filters, or_filters, fields):
|
||||||
|
filters.update({"featured": 1})
|
||||||
|
featured_courses = frappe.get_all(
|
||||||
|
"LMS Course",
|
||||||
|
filters=filters,
|
||||||
|
fields=fields,
|
||||||
|
or_filters=or_filters,
|
||||||
|
order_by="enrollments desc",
|
||||||
|
)
|
||||||
|
return featured_courses
|
||||||
|
|
||||||
|
|
||||||
|
def get_course_fields():
|
||||||
|
return [
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"tags",
|
||||||
|
"image",
|
||||||
|
"short_introduction",
|
||||||
|
"published",
|
||||||
|
"upcoming",
|
||||||
|
"featured",
|
||||||
|
"disable_self_learning",
|
||||||
|
"published_on",
|
||||||
|
"category",
|
||||||
|
"status",
|
||||||
|
"paid_course",
|
||||||
|
"paid_certificate",
|
||||||
|
"course_price",
|
||||||
|
"currency",
|
||||||
|
"amount_usd",
|
||||||
|
"enable_certification",
|
||||||
|
"lessons",
|
||||||
|
"enrollments",
|
||||||
|
"rating",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_course_details(course):
|
def get_course_details(course):
|
||||||
course_details = frappe.db.get_value(
|
course_details = frappe.db.get_value(
|
||||||
@@ -999,6 +1145,7 @@ def get_course_details(course):
|
|||||||
"category",
|
"category",
|
||||||
"status",
|
"status",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
|
"paid_certificate",
|
||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
@@ -1013,7 +1160,7 @@ def get_course_details(course):
|
|||||||
|
|
||||||
course_details.instructors = get_instructors(course_details.name)
|
course_details.instructors = get_instructors(course_details.name)
|
||||||
# course_details.is_instructor = is_instructor(course_details.name)
|
# course_details.is_instructor = is_instructor(course_details.name)
|
||||||
if course_details.paid_course:
|
if course_details.paid_course or course_details.paid_certificate:
|
||||||
"""course_details.course_price, course_details.currency = check_multicurrency(
|
"""course_details.course_price, course_details.currency = check_multicurrency(
|
||||||
course_details.course_price, course_details.currency, None, course_details.amount_usd
|
course_details.course_price, course_details.currency, None, course_details.amount_usd
|
||||||
)"""
|
)"""
|
||||||
@@ -1126,14 +1273,21 @@ def get_lesson(course, chapter, lesson):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
membership = get_membership(course)
|
membership = get_membership(course)
|
||||||
course_title = frappe.db.get_value("LMS Course", course, "title")
|
course_info = frappe.db.get_value(
|
||||||
|
"LMS Course", course, ["title", "paid_certificate"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not lesson_details.include_in_preview
|
not lesson_details.include_in_preview
|
||||||
and not membership
|
and not membership
|
||||||
and not has_course_moderator_role()
|
and not has_course_moderator_role()
|
||||||
and not is_instructor(course)
|
and not is_instructor(course)
|
||||||
):
|
):
|
||||||
return {"no_preview": 1, "title": lesson_details.title, "course_title": course_title}
|
return {
|
||||||
|
"no_preview": 1,
|
||||||
|
"title": lesson_details.title,
|
||||||
|
"course_title": course_info.title,
|
||||||
|
}
|
||||||
|
|
||||||
lesson_details = frappe.db.get_value(
|
lesson_details = frappe.db.get_value(
|
||||||
"Course Lesson",
|
"Course Lesson",
|
||||||
@@ -1168,7 +1322,8 @@ def get_lesson(course, chapter, lesson):
|
|||||||
lesson_details.prev = neighbours["prev"]
|
lesson_details.prev = neighbours["prev"]
|
||||||
lesson_details.membership = membership
|
lesson_details.membership = membership
|
||||||
lesson_details.instructors = get_instructors(course)
|
lesson_details.instructors = get_instructors(course)
|
||||||
lesson_details.course_title = course_title
|
lesson_details.course_title = course_info.title
|
||||||
|
lesson_details.paid_certificate = course_info.paid_certificate
|
||||||
return lesson_details
|
return lesson_details
|
||||||
|
|
||||||
|
|
||||||
@@ -1194,6 +1349,16 @@ def get_neighbour_lesson(course, chapter, lesson):
|
|||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_batch_details(batch):
|
def get_batch_details(batch):
|
||||||
|
batch_students = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not frappe.db.get_value("LMS Batch", batch, "published")
|
||||||
|
and has_student_role()
|
||||||
|
and frappe.session.user not in batch_students
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
batch_details = frappe.db.get_value(
|
batch_details = frappe.db.get_value(
|
||||||
"LMS Batch",
|
"LMS Batch",
|
||||||
batch,
|
batch,
|
||||||
@@ -1215,6 +1380,7 @@ def get_batch_details(batch):
|
|||||||
"paid_batch",
|
"paid_batch",
|
||||||
"evaluation_end_date",
|
"evaluation_end_date",
|
||||||
"allow_self_enrollment",
|
"allow_self_enrollment",
|
||||||
|
"certification",
|
||||||
"timezone",
|
"timezone",
|
||||||
"category",
|
"category",
|
||||||
],
|
],
|
||||||
@@ -1222,13 +1388,12 @@ def get_batch_details(batch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
batch_details.instructors = get_instructors(batch)
|
batch_details.instructors = get_instructors(batch)
|
||||||
|
batch_details.accept_enrollments = batch_details.start_date > getdate()
|
||||||
|
|
||||||
batch_details.courses = frappe.get_all(
|
batch_details.courses = frappe.get_all(
|
||||||
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
|
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
|
||||||
)
|
)
|
||||||
batch_details.students = frappe.get_all(
|
batch_details.students = batch_students
|
||||||
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
|
|
||||||
)
|
|
||||||
|
|
||||||
if batch_details.paid_batch and batch_details.start_date >= getdate():
|
if batch_details.paid_batch and batch_details.start_date >= getdate():
|
||||||
batch_details.amount, batch_details.currency = check_multicurrency(
|
batch_details.amount, batch_details.currency = check_multicurrency(
|
||||||
@@ -1450,7 +1615,7 @@ def get_batch_students(batch):
|
|||||||
)
|
)
|
||||||
detail.assessments[title] = assessment_info
|
detail.assessments[title] = assessment_info
|
||||||
|
|
||||||
if assessment_info.result == "Passed":
|
if assessment_info.result == "Pass":
|
||||||
assessments_completed += 1
|
assessments_completed += 1
|
||||||
|
|
||||||
detail.courses_completed = courses_completed
|
detail.courses_completed = courses_completed
|
||||||
@@ -1493,20 +1658,26 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
|
|||||||
|
|
||||||
attempt = frappe.db.exists(doctype, filters)
|
attempt = frappe.db.exists(doctype, filters)
|
||||||
if attempt:
|
if attempt:
|
||||||
attempt_details = frappe.db.get_value(doctype, filters, fields)
|
fields.append("name")
|
||||||
|
attempt_details = frappe.db.get_value(doctype, filters, fields, as_dict=1)
|
||||||
if assessment_type == "LMS Quiz":
|
if assessment_type == "LMS Quiz":
|
||||||
result = "Failed"
|
result = "Failed"
|
||||||
passing_percentage = frappe.db.get_value(
|
passing_percentage = frappe.db.get_value(
|
||||||
"LMS Quiz", assessment, "passing_percentage"
|
"LMS Quiz", assessment, "passing_percentage"
|
||||||
)
|
)
|
||||||
if attempt_details >= passing_percentage:
|
if attempt_details.percentage >= passing_percentage:
|
||||||
result = "Passed"
|
result = "Pass"
|
||||||
else:
|
else:
|
||||||
result = attempt_details
|
result = attempt_details.status
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
{
|
{
|
||||||
"status": attempt_details,
|
"status": attempt_details.percentage
|
||||||
|
if assessment_type == "LMS Quiz"
|
||||||
|
else attempt_details.status,
|
||||||
"result": result,
|
"result": result,
|
||||||
|
"assessment": assessment,
|
||||||
|
"type": assessment_type,
|
||||||
|
"submission": attempt_details.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1586,11 +1757,19 @@ def get_order_summary(doctype, docname, country=None):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Course",
|
"LMS Course",
|
||||||
docname,
|
docname,
|
||||||
["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"],
|
[
|
||||||
|
"title",
|
||||||
|
"name",
|
||||||
|
"paid_course",
|
||||||
|
"paid_certificate",
|
||||||
|
"course_price as amount",
|
||||||
|
"currency",
|
||||||
|
"amount_usd",
|
||||||
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not details.paid_course:
|
if not details.paid_course and not details.paid_certificate:
|
||||||
raise frappe.throw(_("This course is free."))
|
raise frappe.throw(_("This course is free."))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -1704,9 +1883,14 @@ def update_payment_record(doctype, docname):
|
|||||||
"order_id": data.get("order_id"),
|
"order_id": data.get("order_id"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
payment_for_certificate = frappe.db.get_value(
|
||||||
|
"LMS Payment", data.payment, "payment_for_certificate"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if doctype == "LMS Course":
|
if payment_for_certificate:
|
||||||
|
update_certificate_purchase(docname)
|
||||||
|
elif doctype == "LMS Course":
|
||||||
enroll_in_course(data.payment, docname)
|
enroll_in_course(data.payment, docname)
|
||||||
else:
|
else:
|
||||||
enroll_in_batch(docname, data.payment)
|
enroll_in_batch(docname, data.payment)
|
||||||
@@ -1766,6 +1950,15 @@ def enroll_in_batch(batch, payment_name=None):
|
|||||||
new_student.save()
|
new_student.save()
|
||||||
|
|
||||||
|
|
||||||
|
def update_certificate_purchase(course):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Enrollment",
|
||||||
|
{"member": frappe.session.user, "course": course},
|
||||||
|
"purchased_certificate",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_programs():
|
def get_programs():
|
||||||
if (
|
if (
|
||||||
@@ -1945,3 +2138,25 @@ def get_batch_card_details(batches):
|
|||||||
batch.price = fmt_money(batch.amount, 0, batch.currency)
|
batch.price = fmt_money(batch.amount, 0, batch.currency)
|
||||||
|
|
||||||
return batches
|
return batches
|
||||||
|
|
||||||
|
|
||||||
|
def get_palette(full_name):
|
||||||
|
"""
|
||||||
|
Returns a color unique to each member for Avatar"""
|
||||||
|
|
||||||
|
palette = [
|
||||||
|
["--orange-avatar-bg", "--orange-avatar-color"],
|
||||||
|
["--pink-avatar-bg", "--pink-avatar-color"],
|
||||||
|
["--blue-avatar-bg", "--blue-avatar-color"],
|
||||||
|
["--green-avatar-bg", "--green-avatar-color"],
|
||||||
|
["--dark-green-avatar-bg", "--dark-green-avatar-color"],
|
||||||
|
["--red-avatar-bg", "--red-avatar-color"],
|
||||||
|
["--yellow-avatar-bg", "--yellow-avatar-color"],
|
||||||
|
["--purple-avatar-bg", "--purple-avatar-color"],
|
||||||
|
["--gray-avatar-bg", "--gray-avatar-color0"],
|
||||||
|
]
|
||||||
|
|
||||||
|
encoded_name = str(full_name).encode("utf-8")
|
||||||
|
hash_name = hashlib.md5(encoded_name).hexdigest()
|
||||||
|
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
|
||||||
|
return palette[idx % 8]
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{% set member = frappe.get_doc("User", frappe.session.user) %}
|
|
||||||
<div class="mt-10">
|
|
||||||
{% if member.get_mentored_courses() | length %}
|
|
||||||
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
|
|
||||||
<div class="cards-parent">
|
|
||||||
{% for course in member.get_mentored_courses() %}
|
|
||||||
{{ widgets.CourseCard(course=course) }}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"__unsaved": 1,
|
|
||||||
"creation": "2021-10-21 11:32:57.411626",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Web Template",
|
|
||||||
"fields": [],
|
|
||||||
"idx": 0,
|
|
||||||
"modified": "2021-10-21 12:01:56.270656",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "Courses Mentored",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"standard": 1,
|
|
||||||
"template": "",
|
|
||||||
"type": "Section"
|
|
||||||
}
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{% set color = get_palette(member.full_name) %}
|
|
||||||
<div class="common-card-style member-card">
|
|
||||||
<div class="d-flex">
|
|
||||||
{{ widgets.Avatar(member=member, avatar_class=avatar_class) }}
|
|
||||||
|
|
||||||
<div class="ml-3 my-auto">
|
|
||||||
<div class="member-card-title">
|
|
||||||
{{ member.full_name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if member.headline %}
|
|
||||||
<div> {{ member.headline }} </div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if member.looking_for_job %}
|
|
||||||
<div class="indicator-pill green"> {{ _("Open Network") }} </div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set course_count = get_authored_courses(member.name, True) | length %}
|
|
||||||
{% set suffix = "Courses" if course_count > 1 else "Course" %}
|
|
||||||
|
|
||||||
{% if show_course_count and course_count > 0 %}
|
|
||||||
<div class="">
|
|
||||||
Created {{ course_count }} {{ suffix }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="{{ get_profile_url(member.username) }}"></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
909
lms/locale/ar.po
909
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
2241
lms/locale/bs.po
2241
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
911
lms/locale/de.po
911
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
917
lms/locale/eo.po
917
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
917
lms/locale/es.po
917
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
945
lms/locale/fa.po
945
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
911
lms/locale/fr.po
911
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
909
lms/locale/hu.po
909
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
909
lms/locale/pl.po
909
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
913
lms/locale/ru.po
913
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
921
lms/locale/sv.po
921
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
911
lms/locale/tr.po
911
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
909
lms/locale/zh.po
909
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
|||||||
# Copyright (c) 2021, FOSS United and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
from lms.lms.doctype.lms_course.test_lms_course import new_user
|
|
||||||
|
|
||||||
|
|
||||||
class TestCustomUser(unittest.TestCase):
|
|
||||||
def test_with_basic_username(self):
|
|
||||||
user = new_user("Username", "test_with_basic_username@example.com")
|
|
||||||
self.assertEqual(user.username, "username")
|
|
||||||
|
|
||||||
def test_without_username(self):
|
|
||||||
"""The user in this test has the same first name as the user of the test test_with_basic_username.
|
|
||||||
In such cases frappe makes the username of the second user empty.
|
|
||||||
The condition in lms app should override this and save a username."""
|
|
||||||
user = new_user("Username", "test-without-username@example.com")
|
|
||||||
self.assertTrue(user.username)
|
|
||||||
|
|
||||||
def test_with_short_first_name(self):
|
|
||||||
user = new_user("USN", "test_with_short_first_name@example.com")
|
|
||||||
self.assertGreaterEqual(len(user.username), 4)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls) -> None:
|
|
||||||
users = [
|
|
||||||
"test_with_basic_username@example.com",
|
|
||||||
"test-without-username@example.com",
|
|
||||||
"test_with_short_first_name@example.com",
|
|
||||||
]
|
|
||||||
frappe.db.delete("User", {"name": ["in", users]})
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
import hashlib
|
|
||||||
import frappe
|
|
||||||
import requests
|
|
||||||
from frappe import _
|
|
||||||
from frappe.core.doctype.user.user import User
|
|
||||||
from frappe.utils import cint, escape_html, random_string
|
|
||||||
from frappe.website.utils import is_signup_disabled
|
|
||||||
from lms.lms.utils import get_average_rating, get_country_code
|
|
||||||
from frappe.website.utils import cleanup_page_name
|
|
||||||
from frappe.model.naming import append_number_if_name_exists
|
|
||||||
from lms.widgets import Widgets
|
|
||||||
|
|
||||||
|
|
||||||
class CustomUser(User):
|
|
||||||
def validate(self):
|
|
||||||
super().validate()
|
|
||||||
self.validate_username_duplicates()
|
|
||||||
|
|
||||||
def after_insert(self):
|
|
||||||
super().after_insert()
|
|
||||||
self.add_roles("LMS Student")
|
|
||||||
|
|
||||||
def validate_username_duplicates(self):
|
|
||||||
while not self.username or self.username_exists():
|
|
||||||
self.username = append_number_if_name_exists(
|
|
||||||
self.doctype, cleanup_page_name(self.full_name), fieldname="username"
|
|
||||||
)
|
|
||||||
if " " in self.username:
|
|
||||||
self.username = self.username.replace(" ", "")
|
|
||||||
|
|
||||||
if len(self.username) < 4:
|
|
||||||
self.username = self.email.replace("@", "").replace(".", "")
|
|
||||||
|
|
||||||
def validate_skills(self):
|
|
||||||
unique_skills = []
|
|
||||||
for skill in self.skill:
|
|
||||||
if not skill.skill_name:
|
|
||||||
return
|
|
||||||
if not skill.skill_name in unique_skills:
|
|
||||||
unique_skills.append(skill.skill_name)
|
|
||||||
else:
|
|
||||||
frappe.throw(_("Skills must be unique"))
|
|
||||||
|
|
||||||
def get_batch_count(self) -> int:
|
|
||||||
"""Returns the number of batches authored by this user."""
|
|
||||||
return frappe.db.count(
|
|
||||||
"LMS Enrollment", {"member": self.name, "member_type": "Mentor"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_user_reviews(self):
|
|
||||||
"""Returns the reviews created by user"""
|
|
||||||
return frappe.get_all("LMS Course Review", {"owner": self.name})
|
|
||||||
|
|
||||||
def get_mentored_courses(self):
|
|
||||||
"""Returns all courses mentored by this user"""
|
|
||||||
mentored_courses = []
|
|
||||||
mapping = frappe.get_all(
|
|
||||||
"LMS Course Mentor Mapping",
|
|
||||||
{
|
|
||||||
"mentor": self.name,
|
|
||||||
},
|
|
||||||
["name", "course"],
|
|
||||||
)
|
|
||||||
|
|
||||||
for map in mapping:
|
|
||||||
if frappe.db.get_value("LMS Course", map.course, "published"):
|
|
||||||
course = frappe.db.get_value(
|
|
||||||
"LMS Course",
|
|
||||||
map.course,
|
|
||||||
["name", "upcoming", "title", "image", "enable_certification"],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
mentored_courses.append(course)
|
|
||||||
|
|
||||||
return mentored_courses
|
|
||||||
|
|
||||||
|
|
||||||
def get_enrolled_courses():
|
|
||||||
in_progress = []
|
|
||||||
completed = []
|
|
||||||
memberships = get_course_membership(None, member_type="Student")
|
|
||||||
|
|
||||||
for membership in memberships:
|
|
||||||
course = frappe.db.get_value(
|
|
||||||
"LMS Course",
|
|
||||||
membership.course,
|
|
||||||
[
|
|
||||||
"name",
|
|
||||||
"upcoming",
|
|
||||||
"title",
|
|
||||||
"short_introduction",
|
|
||||||
"image",
|
|
||||||
"enable_certification",
|
|
||||||
"paid_course",
|
|
||||||
"course_price",
|
|
||||||
"currency",
|
|
||||||
"published",
|
|
||||||
"creation",
|
|
||||||
],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
if not course.published:
|
|
||||||
continue
|
|
||||||
course.enrollment_count = frappe.db.count(
|
|
||||||
"LMS Enrollment", {"course": course.name, "member_type": "Student"}
|
|
||||||
)
|
|
||||||
course.avg_rating = get_average_rating(course.name) or 0
|
|
||||||
progress = cint(membership.progress)
|
|
||||||
if progress < 100:
|
|
||||||
in_progress.append(course)
|
|
||||||
else:
|
|
||||||
completed.append(course)
|
|
||||||
|
|
||||||
in_progress.sort(key=lambda x: x.enrollment_count, reverse=True)
|
|
||||||
completed.sort(key=lambda x: x.enrollment_count, reverse=True)
|
|
||||||
|
|
||||||
return {"in_progress": in_progress, "completed": completed}
|
|
||||||
|
|
||||||
|
|
||||||
def get_course_membership(member=None, member_type=None):
|
|
||||||
"""Returns all memberships of the user."""
|
|
||||||
|
|
||||||
filters = {"member": member or frappe.session.user}
|
|
||||||
if member_type:
|
|
||||||
filters["member_type"] = member_type
|
|
||||||
|
|
||||||
return frappe.get_all("LMS Enrollment", filters, ["name", "course", "progress"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_authored_courses(member=None, only_published=True):
|
|
||||||
"""Returns the number of courses authored by this user."""
|
|
||||||
course_details = []
|
|
||||||
courses = frappe.get_all(
|
|
||||||
"Course Instructor", {"instructor": member or frappe.session.user}, ["parent"]
|
|
||||||
)
|
|
||||||
|
|
||||||
for course in courses:
|
|
||||||
detail = frappe.db.get_value(
|
|
||||||
"LMS Course",
|
|
||||||
course.parent,
|
|
||||||
[
|
|
||||||
"name",
|
|
||||||
"upcoming",
|
|
||||||
"title",
|
|
||||||
"short_introduction",
|
|
||||||
"image",
|
|
||||||
"paid_course",
|
|
||||||
"course_price",
|
|
||||||
"currency",
|
|
||||||
"status",
|
|
||||||
"published",
|
|
||||||
"creation",
|
|
||||||
],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if only_published and detail and not detail.published:
|
|
||||||
continue
|
|
||||||
detail.enrollment_count = frappe.db.count(
|
|
||||||
"LMS Enrollment", {"course": detail.name, "member_type": "Student"}
|
|
||||||
)
|
|
||||||
detail.avg_rating = get_average_rating(detail.name) or 0
|
|
||||||
course_details.append(detail)
|
|
||||||
|
|
||||||
course_details.sort(key=lambda x: x.enrollment_count, reverse=True)
|
|
||||||
return course_details
|
|
||||||
|
|
||||||
|
|
||||||
def get_palette(full_name):
|
|
||||||
"""
|
|
||||||
Returns a color unique to each member for Avatar"""
|
|
||||||
|
|
||||||
palette = [
|
|
||||||
["--orange-avatar-bg", "--orange-avatar-color"],
|
|
||||||
["--pink-avatar-bg", "--pink-avatar-color"],
|
|
||||||
["--blue-avatar-bg", "--blue-avatar-color"],
|
|
||||||
["--green-avatar-bg", "--green-avatar-color"],
|
|
||||||
["--dark-green-avatar-bg", "--dark-green-avatar-color"],
|
|
||||||
["--red-avatar-bg", "--red-avatar-color"],
|
|
||||||
["--yellow-avatar-bg", "--yellow-avatar-color"],
|
|
||||||
["--purple-avatar-bg", "--purple-avatar-color"],
|
|
||||||
["--gray-avatar-bg", "--gray-avatar-color0"],
|
|
||||||
]
|
|
||||||
|
|
||||||
encoded_name = str(full_name).encode("utf-8")
|
|
||||||
hash_name = hashlib.md5(encoded_name).hexdigest()
|
|
||||||
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
|
|
||||||
return palette[idx % 8]
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def sign_up(email, full_name, verify_terms, user_category):
|
|
||||||
if is_signup_disabled():
|
|
||||||
frappe.throw(_("Sign Up is disabled"), _("Not Allowed"))
|
|
||||||
|
|
||||||
user = frappe.db.get("User", {"email": email})
|
|
||||||
if user:
|
|
||||||
if user.enabled:
|
|
||||||
return 0, _("Already Registered")
|
|
||||||
else:
|
|
||||||
return 0, _("Registered but disabled")
|
|
||||||
else:
|
|
||||||
if frappe.db.get_creation_count("User", 60) > 300:
|
|
||||||
frappe.respond_as_web_page(
|
|
||||||
_("Temporarily Disabled"),
|
|
||||||
_(
|
|
||||||
"Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
|
||||||
),
|
|
||||||
http_status_code=429,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "User",
|
|
||||||
"email": email,
|
|
||||||
"first_name": escape_html(full_name),
|
|
||||||
"verify_terms": verify_terms,
|
|
||||||
"user_category": user_category,
|
|
||||||
"country": "",
|
|
||||||
"enabled": 1,
|
|
||||||
"new_password": random_string(10),
|
|
||||||
"user_type": "Website User",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
user.flags.ignore_permissions = True
|
|
||||||
user.flags.ignore_password_policy = True
|
|
||||||
user.insert()
|
|
||||||
|
|
||||||
# set default signup role as per Portal Settings
|
|
||||||
default_role = frappe.db.get_value("Portal Settings", None, "default_role")
|
|
||||||
if default_role:
|
|
||||||
user.add_roles(default_role)
|
|
||||||
|
|
||||||
user.add_roles("LMS Student")
|
|
||||||
set_country_from_ip(None, user.name)
|
|
||||||
|
|
||||||
if user.flags.email_sent:
|
|
||||||
return 1, _("Please check your email for verification")
|
|
||||||
else:
|
|
||||||
return 2, _("Please ask your administrator to verify your sign-up")
|
|
||||||
|
|
||||||
|
|
||||||
def set_country_from_ip(login_manager=None, user=None):
|
|
||||||
if not user and login_manager:
|
|
||||||
user = login_manager.user
|
|
||||||
user_country = frappe.db.get_value("User", user, "country")
|
|
||||||
# if user_country:
|
|
||||||
# return
|
|
||||||
frappe.db.set_value("User", user, "country", get_country_code())
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def on_session_creation(login_manager):
|
|
||||||
if frappe.db.get_single_value(
|
|
||||||
"System Settings", "setup_complete"
|
|
||||||
) and frappe.db.get_single_value("LMS Settings", "default_home"):
|
|
||||||
frappe.local.response["home_page"] = "/lms"
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def search_users(start: int = 0, text: str = ""):
|
|
||||||
start = cint(start)
|
|
||||||
search_text = frappe.db.escape(f"%{text}%")
|
|
||||||
|
|
||||||
or_filters = get_or_filters(search_text)
|
|
||||||
count = len(get_users(or_filters, 0, 900000000))
|
|
||||||
users = get_users(or_filters, start, 24)
|
|
||||||
user_details = get_user_details(users)
|
|
||||||
|
|
||||||
return {"user_details": user_details, "start": start + 24, "count": count}
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_filters(text):
|
|
||||||
user_fields = [
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"full_name",
|
|
||||||
"email",
|
|
||||||
"preferred_location",
|
|
||||||
"dream_companies",
|
|
||||||
]
|
|
||||||
education_fields = ["institution_name", "location", "degree_type", "major"]
|
|
||||||
work_fields = ["title", "company"]
|
|
||||||
certification_fields = ["certification_name", "organization"]
|
|
||||||
|
|
||||||
or_filters = []
|
|
||||||
if text:
|
|
||||||
for field in user_fields:
|
|
||||||
or_filters.append(f"u.{field} like {text}")
|
|
||||||
for field in education_fields:
|
|
||||||
or_filters.append(f"ed.{field} like {text}")
|
|
||||||
for field in work_fields:
|
|
||||||
or_filters.append(f"we.{field} like {text}")
|
|
||||||
for field in certification_fields:
|
|
||||||
or_filters.append(f"c.{field} like {text}")
|
|
||||||
|
|
||||||
or_filters.append(f"s.skill_name like {text}")
|
|
||||||
or_filters.append(f"pf.function like {text}")
|
|
||||||
or_filters.append(f"pi.industry like {text}")
|
|
||||||
|
|
||||||
return "AND ({})".format(" OR ".join(or_filters)) if or_filters else ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_details(users):
|
|
||||||
user_details = []
|
|
||||||
for user in users:
|
|
||||||
details = frappe.db.get_value(
|
|
||||||
"User",
|
|
||||||
user,
|
|
||||||
["name", "username", "full_name", "user_image", "headline", "looking_for_job"],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
user_details.append(Widgets().MemberCard(member=details, avatar_class="avatar-large"))
|
|
||||||
|
|
||||||
return user_details
|
|
||||||
|
|
||||||
|
|
||||||
def get_users(or_filters, start, page_length):
|
|
||||||
users = frappe.db.sql(
|
|
||||||
"""
|
|
||||||
SELECT DISTINCT u.name
|
|
||||||
FROM `tabUser` u
|
|
||||||
LEFT JOIN `tabEducation Detail` ed
|
|
||||||
ON u.name = ed.parent
|
|
||||||
LEFT JOIN `tabWork Experience` we
|
|
||||||
ON u.name = we.parent
|
|
||||||
LEFT JOIN `tabCertification` c
|
|
||||||
ON u.name = c.parent
|
|
||||||
LEFT JOIN `tabSkills` s
|
|
||||||
ON u.name = s.parent
|
|
||||||
LEFT JOIN `tabPreferred Function` pf
|
|
||||||
ON u.name = pf.parent
|
|
||||||
LEFT JOIN `tabPreferred Industry` pi
|
|
||||||
ON u.name = pi.parent
|
|
||||||
WHERE u.enabled = True {or_filters}
|
|
||||||
ORDER BY u.creation desc
|
|
||||||
LIMIT {start}, {page_length}
|
|
||||||
""".format(
|
|
||||||
or_filters=or_filters, start=start, page_length=page_length
|
|
||||||
),
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
return users
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def save_role(user, role, value):
|
|
||||||
frappe.only_for("Moderator")
|
|
||||||
if cint(value):
|
|
||||||
doc = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Has Role",
|
|
||||||
"parent": user,
|
|
||||||
"role": role,
|
|
||||||
"parenttype": "User",
|
|
||||||
"parentfield": "roles",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
else:
|
|
||||||
frappe.db.delete("Has Role", {"parent": user, "role": role})
|
|
||||||
return True
|
|
||||||
@@ -6,6 +6,7 @@ import re
|
|||||||
import os
|
import os
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import get_files_path
|
||||||
from frappe.website.page_renderers.base_renderer import BaseRenderer
|
from frappe.website.page_renderers.base_renderer import BaseRenderer
|
||||||
from frappe.website.page_renderers.document_page import DocumentPage
|
from frappe.website.page_renderers.document_page import DocumentPage
|
||||||
from frappe.website.page_renderers.list_page import ListPage
|
from frappe.website.page_renderers.list_page import ListPage
|
||||||
@@ -173,3 +174,23 @@ class SCORMRenderer(BaseRenderer):
|
|||||||
)
|
)
|
||||||
response.mimetype = mimetypes.guess_type(index_path)[0]
|
response.mimetype = mimetypes.guess_type(index_path)[0]
|
||||||
return response
|
return response
|
||||||
|
elif not os.path.exists(path):
|
||||||
|
chapter_folder = "/".join(self.path.split("/")[:3])
|
||||||
|
chapter_folder_path = os.path.realpath(
|
||||||
|
frappe.get_site_path("public", chapter_folder)
|
||||||
|
)
|
||||||
|
file = path.split("/")[-1]
|
||||||
|
correct_file_path = None
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(chapter_folder_path):
|
||||||
|
if file in files:
|
||||||
|
correct_file_path = os.path.join(root, file)
|
||||||
|
break
|
||||||
|
|
||||||
|
if correct_file_path:
|
||||||
|
f = open(correct_file_path, "rb")
|
||||||
|
response = Response(
|
||||||
|
wrap_file(frappe.local.request.environ, f), direct_passthrough=True
|
||||||
|
)
|
||||||
|
response.mimetype = mimetypes.guess_type(correct_file_path)[0]
|
||||||
|
return response
|
||||||
|
|||||||
@@ -99,4 +99,6 @@ lms.patches.v2_0.update_quiz_submission_data
|
|||||||
lms.patches.v2_0.convert_quiz_duration_to_minutes
|
lms.patches.v2_0.convert_quiz_duration_to_minutes
|
||||||
lms.patches.v2_0.allow_guest_access #05-02-2025
|
lms.patches.v2_0.allow_guest_access #05-02-2025
|
||||||
lms.patches.v2_0.migrate_batch_student_data #10-02-2025
|
lms.patches.v2_0.migrate_batch_student_data #10-02-2025
|
||||||
lms.patches.v2_0.delete_old_enrollment_doctypes
|
lms.patches.v2_0.delete_old_enrollment_doctypes
|
||||||
|
lms.patches.v2_0.delete_unused_custom_fields
|
||||||
|
lms.patches.v2_0.update_certificate_request_status
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user