Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6422d1046 | ||
|
|
7196bbe221 | ||
|
|
bed16c3726 | ||
|
|
d18ca232e3 | ||
|
|
d1200d0fa9 | ||
|
|
d1c88b306f | ||
|
|
7f2723f9cb | ||
|
|
8df4bef71a | ||
|
|
aa87622606 | ||
|
|
b91339fe28 | ||
|
|
17d4973ab8 | ||
|
|
3c12548420 | ||
|
|
20c10f1645 | ||
|
|
a7843e0e3a | ||
|
|
169ea4385f | ||
|
|
9549f3a3ed | ||
|
|
ba66c2549f | ||
|
|
76c3e630cc | ||
|
|
7a0b952638 | ||
|
|
5966a3edad | ||
|
|
d44c7cd9fc | ||
|
|
46553987ac | ||
|
|
45725f1f6e | ||
|
|
58369ba65e | ||
|
|
5ce67dda2e | ||
|
|
237ff8db07 | ||
|
|
7da608ed44 | ||
|
|
60f2e86b42 | ||
|
|
b5e67a25d2 | ||
|
|
9d2ef4929c | ||
|
|
050084e552 | ||
|
|
86e9739218 | ||
|
|
bd94890da7 | ||
|
|
965f6adb90 | ||
|
|
4979569cf3 | ||
|
|
5c21a0532a | ||
|
|
a2025c0571 | ||
|
|
e07aae3fb0 | ||
|
|
65d628ffc0 | ||
|
|
bf290bbf0a | ||
|
|
3c9059025b | ||
|
|
4b0413720b | ||
|
|
f8b4ff4bd3 | ||
|
|
3b8ff171f4 | ||
|
|
dec270a10b | ||
|
|
152a339c4e | ||
|
|
395fe700e0 | ||
|
|
ec25e895dc | ||
|
|
e02e4c7ab4 | ||
|
|
e69cc9af1a | ||
|
|
98b8464e1a | ||
|
|
0170fcc111 | ||
|
|
0be5439e81 | ||
|
|
63f857b8fc | ||
|
|
a3b8ed8f91 | ||
|
|
cdd46667f3 | ||
|
|
2f8acea988 | ||
|
|
75f0e5b9f1 | ||
|
|
ce51129e84 | ||
|
|
86aa8b0a2a | ||
|
|
aeae62a45c | ||
|
|
6b12df44a0 |
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
@@ -99,6 +99,7 @@ jobs:
|
|||||||
cd ~/frappe-bench/
|
cd ~/frappe-bench/
|
||||||
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
|
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
|
||||||
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
|
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
|
||||||
|
bench --site lms.test set-password frappe@example.com admin
|
||||||
|
|
||||||
- name: cypress pre-requisites
|
- name: cypress pre-requisites
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
|||||||
[submodule "frappe-ui"]
|
[submodule "frappe-ui"]
|
||||||
path = frappe-ui
|
path = frappe-ui
|
||||||
url = https://github.com/pateljannat/frappe-ui
|
url = https://github.com/frappe/frappe-ui
|
||||||
|
|||||||
@@ -61,21 +61,7 @@ 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")
|
|
||||||
.click()
|
|
||||||
.invoke("text", "https://www.youtube.com/watch?v=GoDtyItReto"); */
|
|
||||||
/* cy.get("#content .ce-block")
|
|
||||||
.click()
|
|
||||||
.paste("https://www.youtube.com/watch?v=GoDtyItReto"); */
|
|
||||||
|
|
||||||
cy.fixture("Youtube.mov", "base64").then((fileContent) => {
|
|
||||||
cy.get('input[type="file"]').attachFile({
|
|
||||||
fileContent,
|
|
||||||
fileName: "Youtube.mov",
|
|
||||||
mimeType: "image/png",
|
|
||||||
encoding: "base64",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
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."
|
"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."
|
||||||
);
|
);
|
||||||
@@ -119,12 +105,6 @@ describe("Course Creation", () => {
|
|||||||
cy.url().should("include", "/learn/1-1");
|
cy.url().should("include", "/learn/1-1");
|
||||||
cy.get("div").contains("Test Lesson");
|
cy.get("div").contains("Test Lesson");
|
||||||
|
|
||||||
cy.get("video")
|
|
||||||
.should("be.visible")
|
|
||||||
.children("source")
|
|
||||||
.invoke("attr", "src")
|
|
||||||
.should("include", "/files/Youtube");
|
|
||||||
|
|
||||||
cy.get("div").contains(
|
cy.get("div").contains(
|
||||||
"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."
|
"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."
|
||||||
);
|
);
|
||||||
|
|||||||
1
frappe-ui
Submodule
1
frappe-ui
Submodule
Submodule frappe-ui added at 8cd9b06a5e
@@ -21,7 +21,7 @@
|
|||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.56",
|
"frappe-ui": "^0.1.69",
|
||||||
"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",
|
||||||
|
|||||||
BIN
frontend/public/Quiz.mp4
Normal file
BIN
frontend/public/Quiz.mp4
Normal file
Binary file not shown.
BIN
frontend/public/Upload.mp4
Normal file
BIN
frontend/public/Upload.mp4
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/public/Youtube.mp4
Normal file
BIN
frontend/public/Youtube.mp4
Normal file
Binary file not shown.
67
frontend/src/components/Apps.vue
Normal file
67
frontend/src/components/Apps.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<Popover placement="right-start" class="flex w-full">
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-gray-800 hover:bg-gray-100',
|
||||||
|
]"
|
||||||
|
@click.prevent="togglePopover()"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<LayoutGrid class="size-4 stroke-1.5" />
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
{{ __('Apps') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight class="h-4 w-4 stroke-1.5" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-3 justify-between mx-3 p-2 rounded-lg border border-gray-100 bg-white shadow-xl"
|
||||||
|
>
|
||||||
|
<div v-for="app in apps.data" key="name">
|
||||||
|
<a
|
||||||
|
:href="app.route"
|
||||||
|
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-3 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<img class="size-8" :src="app.logo" />
|
||||||
|
<div class="text-sm" @click="app.onClick">
|
||||||
|
{{ app.title }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Popover, createResource } from 'frappe-ui'
|
||||||
|
import { LayoutGrid, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const apps = createResource({
|
||||||
|
url: 'frappe.apps.get_apps',
|
||||||
|
cache: 'apps',
|
||||||
|
auto: true,
|
||||||
|
transform: (data) => {
|
||||||
|
let _apps = [
|
||||||
|
{
|
||||||
|
name: 'frappe',
|
||||||
|
logo: '/assets/lms/images/desk.png',
|
||||||
|
title: __('Desk'),
|
||||||
|
route: '/app',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
data.map((app) => {
|
||||||
|
if (app.name === 'lms') return
|
||||||
|
_apps.push({
|
||||||
|
name: app.name,
|
||||||
|
logo: app.logo,
|
||||||
|
title: __(app.title),
|
||||||
|
route: app.route,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return _apps
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="flex items-center justify-between">
|
||||||
{{ __('Assessments') }}
|
<div class="text-lg font-semibold mb-4">
|
||||||
|
{{ __('Assessments') }}
|
||||||
|
</div>
|
||||||
|
<Button v-if="canSeeAddButton()" @click="showModal = true">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ __('Add') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="assessments.data?.length">
|
<div v-if="assessments.data?.length">
|
||||||
<ListView
|
<ListView
|
||||||
@@ -9,41 +17,76 @@
|
|||||||
:rows="assessments.data"
|
:rows="assessments.data"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{
|
:options="{
|
||||||
selectable: false,
|
|
||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
getRowRoute: (row) => {
|
getRowRoute: (row) => getRowRoute(row),
|
||||||
if (row.submission) {
|
|
||||||
return {
|
|
||||||
name: 'AssignmentSubmission',
|
|
||||||
params: {
|
|
||||||
assignmentName: row.assessment_name,
|
|
||||||
submissionName: row.submission.name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
name: 'AssignmentSubmission',
|
|
||||||
params: {
|
|
||||||
assignmentName: row.assessment_name,
|
|
||||||
submissionName: 'new',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<ListHeader
|
||||||
|
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||||
|
>
|
||||||
|
<ListHeaderItem :item="item" v-for="item in getAssessmentColumns()">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<component
|
||||||
|
v-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
class="h-4 w-4 stroke-1.5 ml-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ListHeaderItem>
|
||||||
|
</ListHeader>
|
||||||
|
<ListRows>
|
||||||
|
<ListRow :row="row" v-for="row in assessments.data">
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
|
<div>
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
|
</ListRows>
|
||||||
|
<ListSelectBanner>
|
||||||
|
<template #actions="{ unselectAll, selections }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
@click="removeAssessments(selections, unselectAll)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-gray-600">
|
<div v-else class="text-sm italic text-gray-600">
|
||||||
{{ __('No Assessments') }}
|
{{ __('No Assessments') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AssessmentModal
|
||||||
|
v-model="showModal"
|
||||||
|
v-model:assessments="assessments"
|
||||||
|
:batch="props.batch"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ListView, createResource } from 'frappe-ui'
|
import {
|
||||||
import { inject } from 'vue'
|
ListView,
|
||||||
|
ListRow,
|
||||||
|
ListRows,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRowItem,
|
||||||
|
ListSelectBanner,
|
||||||
|
createResource,
|
||||||
|
Button,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { inject, ref } from 'vue'
|
||||||
|
import AssessmentModal from '@/components/Modals/AssessmentModal.vue'
|
||||||
|
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const showModal = ref(false)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
@@ -74,6 +117,61 @@ const assessments = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deleteAssessments = createResource({
|
||||||
|
url: 'lms.lms.api.delete_documents',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'LMS Assessment',
|
||||||
|
documents: values.assessments,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeAssessments = (selections, unselectAll) => {
|
||||||
|
deleteAssessments.submit(
|
||||||
|
{ assessments: Array.from(selections) },
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
assessments.reload()
|
||||||
|
unselectAll()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRowRoute = (row) => {
|
||||||
|
if (row.assessment_type == 'LMS Assignment') {
|
||||||
|
if (row.submission) {
|
||||||
|
return {
|
||||||
|
name: 'AssignmentSubmission',
|
||||||
|
params: {
|
||||||
|
assignmentName: row.assessment_name,
|
||||||
|
submissionName: row.submission.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: 'AssignmentSubmission',
|
||||||
|
params: {
|
||||||
|
assignmentName: row.assessment_name,
|
||||||
|
submissionName: 'new',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: 'Quiz',
|
||||||
|
params: {
|
||||||
|
quizID: row.assessment_name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSeeAddButton = () => {
|
||||||
|
return user.data?.is_moderator || user.data?.is_evaluator
|
||||||
|
}
|
||||||
|
|
||||||
const getAssessmentColumns = () => {
|
const getAssessmentColumns = () => {
|
||||||
let columns = [
|
let columns = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,15 +4,11 @@
|
|||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ __('Courses') }}
|
{{ __('Courses') }}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-if="canSeeAddButton()" @click="openCourseModal()">
|
||||||
v-if="user.data?.is_moderator"
|
|
||||||
variant="solid"
|
|
||||||
@click="openCourseModal()"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
{{ __('Add Course') }}
|
{{ __('Add') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="courses.data?.length">
|
<div v-if="courses.data?.length">
|
||||||
@@ -88,6 +84,7 @@ import {
|
|||||||
ListRowItem,
|
ListRowItem,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||||
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
const showCourseModal = ref(false)
|
const showCourseModal = ref(false)
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -132,23 +129,32 @@ const getCoursesColumns = () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeCourse = createResource({
|
const deleteCourses = createResource({
|
||||||
url: 'frappe.client.delete',
|
url: 'lms.lms.api.delete_documents',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: 'Batch Course',
|
doctype: 'Batch Course',
|
||||||
name: values.course,
|
documents: values.courses,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeCourses = (selections, unselectAll) => {
|
const removeCourses = (selections, unselectAll) => {
|
||||||
selections.forEach(async (course) => {
|
deleteCourses.submit(
|
||||||
removeCourse.submit({ course })
|
{
|
||||||
})
|
courses: Array.from(selections),
|
||||||
setTimeout(() => {
|
},
|
||||||
courses.reload()
|
{
|
||||||
unselectAll()
|
onSuccess(data) {
|
||||||
}, 1000)
|
courses.reload()
|
||||||
|
showToast(__('Success'), __('Courses deleted successfully'), 'check')
|
||||||
|
unselectAll()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSeeAddButton = () => {
|
||||||
|
return user.data?.is_moderator || user.data?.is_evaluator
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Button class="float-right mb-3" variant="solid" @click="openStudentModal()">
|
<Button class="float-right mb-3" @click="openStudentModal()">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
{{ __('Add Student') }}
|
{{ __('Add') }}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Students') }}
|
{{ __('Students') }}
|
||||||
@@ -88,6 +88,7 @@ import {
|
|||||||
import { Trash2, Plus } from 'lucide-vue-next'
|
import { Trash2, Plus } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import StudentModal from '@/components/Modals/StudentModal.vue'
|
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||||
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
const showStudentModal = ref(false)
|
const showStudentModal = ref(false)
|
||||||
|
|
||||||
@@ -135,23 +136,28 @@ const openStudentModal = () => {
|
|||||||
showStudentModal.value = true
|
showStudentModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeStudent = createResource({
|
const deleteStudents = createResource({
|
||||||
url: 'frappe.client.delete',
|
url: 'lms.lms.api.delete_documents',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: 'Batch Student',
|
doctype: 'Batch Student',
|
||||||
name: values.student,
|
documents: values.students,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeStudents = (selections, unselectAll) => {
|
const removeStudents = (selections, unselectAll) => {
|
||||||
selections.forEach(async (student) => {
|
deleteStudents.submit(
|
||||||
removeStudent.submit({ student })
|
{
|
||||||
})
|
students: Array.from(selections),
|
||||||
setTimeout(() => {
|
},
|
||||||
students.reload()
|
{
|
||||||
unselectAll()
|
onSuccess(data) {
|
||||||
}, 500)
|
students.reload()
|
||||||
|
showToast(__('Success'), __('Students deleted successfully'), 'check')
|
||||||
|
unselectAll()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ const options = createResource({
|
|||||||
url: 'frappe.desk.search.search_link',
|
url: 'frappe.desk.search.search_link',
|
||||||
cache: [props.doctype, text.value],
|
cache: [props.doctype, text.value],
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
auto: true,
|
||||||
params: {
|
params: {
|
||||||
txt: text.value,
|
txt: text.value,
|
||||||
doctype: props.doctype,
|
doctype: props.doctype,
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex text-center">
|
<div class="space-y-1">
|
||||||
<div v-for="index in 5">
|
<label class="block text-xs text-gray-600" v-if="props.label">
|
||||||
<Star
|
{{ props.label }}
|
||||||
:class="index <= rating ? 'fill-orange-500' : ''"
|
</label>
|
||||||
class="h-6 w-6 fill-gray-400 text-gray-50 mr-1 cursor-pointer"
|
<div class="flex text-center">
|
||||||
@click="markRating(index)"
|
<div
|
||||||
/>
|
v-for="index in 5"
|
||||||
|
@mouseover="hoveredRating = index"
|
||||||
|
@mouseleave="hoveredRating = 0"
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
class="fill-gray-400 text-gray-50 stroke-1 mr-1 cursor-pointer"
|
||||||
|
:class="iconClasses(index)"
|
||||||
|
@click="markRating(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Star } from 'lucide-vue-next'
|
import { Star } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
@@ -23,10 +32,36 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'md',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const iconClasses = (index) => {
|
||||||
|
let classes = [
|
||||||
|
{
|
||||||
|
sm: 'size-4',
|
||||||
|
md: 'size-5',
|
||||||
|
lg: 'size-6',
|
||||||
|
xl: 'size-7',
|
||||||
|
}[props.size],
|
||||||
|
]
|
||||||
|
if (index <= hoveredRating.value && index > rating.value) {
|
||||||
|
classes.push('fill-yellow-200')
|
||||||
|
} else if (index <= rating.value) {
|
||||||
|
classes.push('fill-yellow-500')
|
||||||
|
}
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
let rating = ref(props.modelValue)
|
const rating = ref(props.modelValue)
|
||||||
|
const hoveredRating = ref(0)
|
||||||
|
|
||||||
let emitChange = (value) => {
|
let emitChange = (value) => {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
@@ -36,4 +71,11 @@ function markRating(index) {
|
|||||||
emitChange(index)
|
emitChange(index)
|
||||||
rating.value = index
|
rating.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal) => {
|
||||||
|
rating.value = newVal
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ import { BookOpen, Users, Star } from 'lucide-vue-next'
|
|||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource } from 'frappe-ui'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast } from '@/utils/'
|
||||||
|
import { capture } from '@/telemetry'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -155,6 +156,9 @@ function enrollStudent() {
|
|||||||
course: props.course.data.name,
|
course: props.course.data.name,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
capture('enrolled_in_course', {
|
||||||
|
course: props.course.data.name,
|
||||||
|
})
|
||||||
createToast({
|
createToast({
|
||||||
title: 'Enrolled Successfully',
|
title: 'Enrolled Successfully',
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
v-if="title && (outline.data?.length || allowEdit)"
|
v-if="title && (outline.data?.length || allowEdit)"
|
||||||
class="grid grid-cols-[70%,30%] mb-4 px-2"
|
class="grid grid-cols-[70%,30%] mb-4 px-2"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-lg">
|
<div class="font-semibold text-lg leading-5">
|
||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
||||||
|
|||||||
74
frontend/src/components/LessonHelp.vue
Normal file
74
frontend/src/components/LessonHelp.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
||||||
|
@click="openHelpDialog('quiz')"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ __('How to add a Quiz?') }}
|
||||||
|
</span>
|
||||||
|
<Info class="w-3 h-3 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 mb-1 leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Click on the add icon in the editor and select Quiz from the menu. It opens up a dialog, where you can either select a quiz from the list or create a new quiz. When you select the Create New option it redirects you to the quiz creation page.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
||||||
|
@click="openHelpDialog('upload')"
|
||||||
|
>
|
||||||
|
<span class="leading-5">
|
||||||
|
{{ __('How to upload content from your system?') }}
|
||||||
|
</span>
|
||||||
|
<Info class="w-3 h-3 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 mb-1 leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'To upload Image, Video, Audio or PDF from your system, click on the add icon and select upload from the menu. Then choose the file you want to add to the lesson and it gets added to your lesson.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
||||||
|
@click="openHelpDialog('youtube')"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ __('How to add a YouTube Video?') }}
|
||||||
|
</span>
|
||||||
|
<Info class="w-3 h-3 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 mb-1 leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Copy the URL of the video from YouTube and paste it in the editor.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ExplanationVideos v-model="showExplanation" :type="type" />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Info } from 'lucide-vue-next'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ExplanationVideos from '@/components/Modals/ExplanationVideos.vue'
|
||||||
|
|
||||||
|
const showExplanation = ref(false)
|
||||||
|
const type = ref(null)
|
||||||
|
|
||||||
|
const openHelpDialog = (contentType) => {
|
||||||
|
type.value = contentType
|
||||||
|
showExplanation.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-lg font-semibold">
|
|
||||||
{{ __('Components') }}
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 space-y-4">
|
|
||||||
<Tooltip
|
|
||||||
:text="
|
|
||||||
__(
|
|
||||||
'Content such as quiz, video and image will be added in the editor you select.'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<div class="">
|
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{{ __('Select an Editor') }}
|
|
||||||
</div>
|
|
||||||
<Select v-model="currentEditor" :options="getEditorOptions()" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<div class="flex">
|
|
||||||
<Link
|
|
||||||
:value="quiz"
|
|
||||||
class="flex-1"
|
|
||||||
doctype="LMS Quiz"
|
|
||||||
:label="__('Add an existing quiz')"
|
|
||||||
@change="(option) => addQuiz(option)"
|
|
||||||
/>
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'QuizCreation',
|
|
||||||
params: {
|
|
||||||
quizID: 'new',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
class="self-end ml-2"
|
|
||||||
>
|
|
||||||
<Button>
|
|
||||||
<template #icon>
|
|
||||||
<Plus class="h-4 w-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{{ __('Add an image, video, pdf or audio.') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<FileUploader
|
|
||||||
v-if="!file"
|
|
||||||
:fileTypes="['image/*', 'video/*', 'audio/*', '.pdf']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(data) => addFile(data)"
|
|
||||||
>
|
|
||||||
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
|
||||||
<div class="">
|
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
|
||||||
{{
|
|
||||||
uploading
|
|
||||||
? __('Uploading {0}%').format(progress)
|
|
||||||
: __('Upload a File')
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="border rounded-md p-2 mr-2">
|
|
||||||
<FileText class="h-4 w-4 stroke-1.5 text-gray-700" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-xs">
|
|
||||||
{{ file.file_name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'To add a YouTube video, paste the URL of the video in the editor.'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<YouTubeExplanation>
|
|
||||||
<template v-slot="{ togglePopover }">
|
|
||||||
<div
|
|
||||||
@click="togglePopover()"
|
|
||||||
class="flex items-center text-sm underline cursor-pointer"
|
|
||||||
>
|
|
||||||
<Info class="w-3 h-3 stroke-1.5 text-gray-700 mr-1" />
|
|
||||||
{{ __('Learn More') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</YouTubeExplanation>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { FileUploader, Button, Select, Tooltip } from 'frappe-ui'
|
|
||||||
import { Plus, FileText, Info } from 'lucide-vue-next'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import YouTubeExplanation from '@/components/Modals/YouTubeExplanation.vue'
|
|
||||||
|
|
||||||
const quiz = ref(null)
|
|
||||||
const file = ref(null)
|
|
||||||
const lessonEditor = ref(null)
|
|
||||||
const instructorEditor = ref(null)
|
|
||||||
const currentEditor = ref('Lesson Content')
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
editor: {
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
notesEditor: {
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const addQuiz = (value) => {
|
|
||||||
getCurrentEditor().caret.setToLastBlock('end', 0)
|
|
||||||
if (value) {
|
|
||||||
getCurrentEditor().blocks.insert('quiz', {
|
|
||||||
quiz: value,
|
|
||||||
})
|
|
||||||
quiz.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addFile = (data) => {
|
|
||||||
getCurrentEditor().caret.setToLastBlock('end', 0)
|
|
||||||
getCurrentEditor().blocks.insert('upload', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateFile = (file) => {
|
|
||||||
let extension = file.name.split('.').pop().toLowerCase()
|
|
||||||
if (!['jpg', 'jpeg', 'png', 'mp4', 'mov', 'mp3', 'pdf'].includes(extension)) {
|
|
||||||
return 'Only image and video files are allowed.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEditorOptions = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'Lesson Content',
|
|
||||||
value: 'Lesson Content',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Instructor Content',
|
|
||||||
value: 'Instructor Content',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCurrentEditor = () => {
|
|
||||||
return currentEditor.value == 'Lesson Content'
|
|
||||||
? lessonEditor.value
|
|
||||||
: instructorEditor.value
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.editor, props.notesEditor],
|
|
||||||
([newEditor, newNotesEditor], [oldEditor, oldNotesEditor]) => {
|
|
||||||
lessonEditor.value = newEditor
|
|
||||||
instructorEditor.value = newNotesEditor
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
203
frontend/src/components/Members.vue
Normal file
203
frontend/src/components/Members.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex min-h-0 flex-col text-base">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-xl font-semibold mb-1">
|
||||||
|
{{ __(label) }}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="text-xs text-gray-600">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
<div class="flex item-center space-x-2">
|
||||||
|
<FormControl
|
||||||
|
v-model="search"
|
||||||
|
:placeholder="__('Search')"
|
||||||
|
type="text"
|
||||||
|
:debounce="300"
|
||||||
|
/>
|
||||||
|
<Button @click="() => (showForm = !showForm)">
|
||||||
|
<template #icon>
|
||||||
|
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
||||||
|
<X v-else class="h-3 w-3 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form to add new member -->
|
||||||
|
<div v-if="showForm" class="flex items-center space-x-2 my-4">
|
||||||
|
<FormControl
|
||||||
|
v-model="member.email"
|
||||||
|
:placeholder="__('Email')"
|
||||||
|
type="email"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="member.first_name"
|
||||||
|
:placeholder="__('First Name')"
|
||||||
|
type="test"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<Button @click="addMember()" variant="subtle">
|
||||||
|
{{ __('Add') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 pb-10 overflow-auto">
|
||||||
|
<!-- Member list -->
|
||||||
|
<div class="overflow-y-scroll">
|
||||||
|
<ul class="divide-y">
|
||||||
|
<li
|
||||||
|
v-for="member in memberList"
|
||||||
|
class="grid grid-cols-3 gap-10 py-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
@click="openProfile(member.username)"
|
||||||
|
class="flex items-center space-x-3 col-span-2"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
:image="member.user_image"
|
||||||
|
:label="member.full_name"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="text-gray-900">
|
||||||
|
{{ member.full_name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="getRole(member)">
|
||||||
|
{{ getRole(member) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-700">
|
||||||
|
{{ member.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center text-gray-700 text-sm">
|
||||||
|
<div v-if="member.last_active">
|
||||||
|
{{ dayjs(member.last_active).format('DD MMM, YYYY HH:mm a') }}
|
||||||
|
</div>
|
||||||
|
<div v-else>-</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="memberList.length && hasNextPage"
|
||||||
|
class="flex justify-center mt-4"
|
||||||
|
>
|
||||||
|
<Button @click="members.reload()">
|
||||||
|
<template #prefix>
|
||||||
|
<RefreshCw class="h-3 w-3 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ref, watch, reactive, inject } from 'vue'
|
||||||
|
import { RefreshCw, Plus, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const show = defineModel('show')
|
||||||
|
const search = ref('')
|
||||||
|
const start = ref(0)
|
||||||
|
const memberList = ref([])
|
||||||
|
const hasNextPage = ref(false)
|
||||||
|
const showForm = ref(false)
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
const member = reactive({
|
||||||
|
email: '',
|
||||||
|
first_name: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const members = createResource({
|
||||||
|
url: 'lms.lms.api.get_members',
|
||||||
|
makeParams: () => {
|
||||||
|
return {
|
||||||
|
search: search.value,
|
||||||
|
start: start.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
memberList.value = memberList.value.concat(data)
|
||||||
|
start.value = start.value + 20
|
||||||
|
hasNextPage.value = data.length === 20
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const openProfile = (username) => {
|
||||||
|
show.value = false
|
||||||
|
router.push({
|
||||||
|
name: 'Profile',
|
||||||
|
params: {
|
||||||
|
username: username,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMember = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'User',
|
||||||
|
first_name: member.first_name,
|
||||||
|
email: member.email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
onSuccess(data) {
|
||||||
|
show.value = false
|
||||||
|
router.push({
|
||||||
|
name: 'Profile',
|
||||||
|
params: {
|
||||||
|
username: data.username,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const addMember = () => {
|
||||||
|
newMember.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(search, () => {
|
||||||
|
memberList.value = []
|
||||||
|
start.value = 0
|
||||||
|
members.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getRole = (role) => {
|
||||||
|
const map = {
|
||||||
|
'LMS Student': 'Student',
|
||||||
|
'Course Creator': 'Instructor',
|
||||||
|
Moderator: 'Moderator',
|
||||||
|
'Batch Evaluator': 'Evaluator',
|
||||||
|
}
|
||||||
|
return map[role]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
86
frontend/src/components/Modals/AssessmentModal.vue
Normal file
86
frontend/src/components/Modals/AssessmentModal.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Add an assessment'),
|
||||||
|
size: 'sm',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Submit'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: (close) => addAssessment(close),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
:options="assessmentTypes"
|
||||||
|
v-model="assessmentType"
|
||||||
|
:label="__('Type')"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-model="assessment"
|
||||||
|
:doctype="assessmentType"
|
||||||
|
:label="__('Assessment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Dialog, FormControl, createResource } from 'frappe-ui'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { showToast } from '@/utils'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const assessmentType = ref(null)
|
||||||
|
const assessment = ref(null)
|
||||||
|
const assessments = defineModel('assessments')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
batch: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const assessmentResource = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'LMS Assessment',
|
||||||
|
parent: props.batch,
|
||||||
|
parenttype: 'LMS Batch',
|
||||||
|
parentfield: 'assessment',
|
||||||
|
assessment_type: assessmentType.value,
|
||||||
|
assessment_name: assessment.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const addAssessment = (close) => {
|
||||||
|
assessmentResource.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
assessments.value.reload()
|
||||||
|
showToast(__('Success'), __('Assessment added successfully'), 'check')
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const assessmentTypes = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: 'Quiz', value: 'LMS Quiz' },
|
||||||
|
{ label: 'Assignment', value: 'LMS Assignment' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -15,18 +15,24 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<FormControl label="Title" v-model="chapter.title" class="mb-4" />
|
<FormControl
|
||||||
|
ref="chapterInput"
|
||||||
|
label="Title"
|
||||||
|
v-model="chapter.title"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FormControl, createResource } from 'frappe-ui'
|
import { Dialog, FormControl, createResource } from 'frappe-ui'
|
||||||
import { defineModel, reactive, watch } from 'vue'
|
import { defineModel, reactive, watch, ref } from 'vue'
|
||||||
import { createToast } from '@/utils/'
|
import { createToast } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const outline = defineModel('outline')
|
const outline = defineModel('outline')
|
||||||
|
const chapterInput = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
course: {
|
course: {
|
||||||
@@ -37,6 +43,7 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const chapter = reactive({
|
const chapter = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
})
|
})
|
||||||
@@ -97,6 +104,7 @@ const addChapter = (close) => {
|
|||||||
{ name: data.name },
|
{ name: data.name },
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
chapter.title = ''
|
||||||
outline.value.reload()
|
outline.value.reload()
|
||||||
createToast({
|
createToast({
|
||||||
text: 'Chapter added successfully',
|
text: 'Chapter added successfully',
|
||||||
@@ -160,4 +168,12 @@ watch(
|
|||||||
chapter.title = newChapter?.title
|
chapter.title = newChapter?.title
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(show, () => {
|
||||||
|
if (show.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
chapterInput.value.$el.querySelector('input').focus()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
378
frontend/src/components/Modals/Event.vue
Normal file
378
frontend/src/components/Modals/Event.vue
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
size: '2xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="flex text-base">
|
||||||
|
<div class="flex flex-col w-1/2 p-5">
|
||||||
|
<div class="text-lg font-semibold mb-4">
|
||||||
|
{{ event.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-4 text-sm text-gray-800">
|
||||||
|
<Tooltip :text="__('Email ID')">
|
||||||
|
<div class="flex items-center space-x-2 w-fit">
|
||||||
|
<User class="h-4 w-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ event.member }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :text="__('Course')">
|
||||||
|
<div class="flex items-center space-x-2 w-fit">
|
||||||
|
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ event.course_title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip v-if="event.batch_title" :text="__('Batch')">
|
||||||
|
<div class="flex items-center space-x-2 w-fit">
|
||||||
|
<Users class="h-4 w-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ event.batch_title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :text="__('Date')">
|
||||||
|
<div class="flex items-center space-x-2 w-fit">
|
||||||
|
<Calendar class="h-4 w-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ dayjs(event.date).format('DD MMM YYYY') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :text="__('Time')">
|
||||||
|
<div class="flex items-center space-x-2 w-fit">
|
||||||
|
<Clock class="h-4 w-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ formatTime(event.start_time) }} -
|
||||||
|
{{ formatTime(event.end_time) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 mt-auto">
|
||||||
|
<Button
|
||||||
|
v-if="certificate.name"
|
||||||
|
@click="openCertificate(certificate)"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FileText class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('View Certificate') }}
|
||||||
|
</Button>
|
||||||
|
<Button v-else @click="openCallLink(event.venue)" class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<Video class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
<span>
|
||||||
|
{{ __('Join Meeting') }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tabs :tabs="tabs" v-model="tabIndex" class="border-l w-1/2">
|
||||||
|
<template #default="{ tab }">
|
||||||
|
<div
|
||||||
|
v-if="tab.label == 'Evaluation'"
|
||||||
|
class="flex flex-col space-y-4 p-5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Rating v-model="evaluation.rating" :label="__('Rating')" />
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
:options="statusOptions"
|
||||||
|
v-model="evaluation.status"
|
||||||
|
:label="__('Status')"
|
||||||
|
class="w-1/2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
v-model="evaluation.summary"
|
||||||
|
:label="__('Summary')"
|
||||||
|
:rows="7"
|
||||||
|
/>
|
||||||
|
<Button variant="solid" @click="saveEvaluation()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col space-y-4 p-5">
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="certificate.published"
|
||||||
|
:label="__('Published')"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-model="certificate.template"
|
||||||
|
:label="__('Template')"
|
||||||
|
doctype="Print Format"
|
||||||
|
:filters="{
|
||||||
|
doc_type: 'LMS Certificate',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="date"
|
||||||
|
v-model="certificate.issue_date"
|
||||||
|
:label="__('Issue Date')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="date"
|
||||||
|
v-model="certificate.expiry_date"
|
||||||
|
:label="__('Expiry Date')"
|
||||||
|
/>
|
||||||
|
<Button variant="solid" @click="saveCertificate()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
createResource,
|
||||||
|
Tabs,
|
||||||
|
Tooltip,
|
||||||
|
Textarea,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
Video,
|
||||||
|
BookOpen,
|
||||||
|
FileText,
|
||||||
|
GraduationCap,
|
||||||
|
Users,
|
||||||
|
ClipboardList,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { inject, reactive, watch, ref, computed } from 'vue'
|
||||||
|
import { formatTime, showToast } from '@/utils'
|
||||||
|
import Rating from '@/components/Controls/Rating.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
const tabIndex = ref(0)
|
||||||
|
const showCertification = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: [Object, null],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const evaluation = reactive({})
|
||||||
|
|
||||||
|
const certificate = reactive({})
|
||||||
|
|
||||||
|
const defaultTemplate = createResource({
|
||||||
|
url: 'frappe.client.get_value',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'Property Setter',
|
||||||
|
fieldname: 'value',
|
||||||
|
filters: {
|
||||||
|
doc_type: 'LMS Certificate',
|
||||||
|
property: 'default_print_format',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
onSuccess(data) {
|
||||||
|
certificate.template = data.value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const openCallLink = (link) => {
|
||||||
|
window.open(link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluationResource = createResource({
|
||||||
|
url: 'lms.lms.api.save_evaluation_details',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
member: props.event.member,
|
||||||
|
course: props.event.course,
|
||||||
|
batch_name: props.event.batch_name,
|
||||||
|
date: props.event.date,
|
||||||
|
start_time: props.event.start_time,
|
||||||
|
end_time: props.event.end_time,
|
||||||
|
status: evaluation.status,
|
||||||
|
rating: evaluation.rating,
|
||||||
|
summary: evaluation.summary,
|
||||||
|
evaluator: props.event.evaluator,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
onSuccess(data) {
|
||||||
|
evaluation.name = data.name
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const evaluationDetails = createResource({
|
||||||
|
url: 'frappe.client.get',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'LMS Certificate Evaluation',
|
||||||
|
filters: {
|
||||||
|
member: props.event.member,
|
||||||
|
course: props.event.course,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
for (const key in data) {
|
||||||
|
if (key in evaluation) evaluation[key] = data[key]
|
||||||
|
if (key == 'rating') evaluation.rating = data.rating * 5
|
||||||
|
if (evaluation.status == 'Pass') showCertification.value = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveEvaluation = () => {
|
||||||
|
evaluationResource.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
if (evaluation.status == 'Pass') {
|
||||||
|
showCertification.value = true
|
||||||
|
} else {
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
showToast(__('Success'), __('Evaluation saved successfully'), 'check')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateResource = createResource({
|
||||||
|
url: 'lms.lms.api.save_certificate_details',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
member: props.event.member,
|
||||||
|
course: props.event.course,
|
||||||
|
batch_name: props.event.batch_name,
|
||||||
|
published: certificate.published,
|
||||||
|
issue_date: certificate.issue_date,
|
||||||
|
expiry_date: certificate.expiry_date,
|
||||||
|
template: certificate.template,
|
||||||
|
evaluator: props.event.evaluator,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
onSuccess(data) {
|
||||||
|
certificate.name = data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const certificateDetails = createResource({
|
||||||
|
url: 'frappe.client.get',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doctype: 'LMS Certificate',
|
||||||
|
filters: {
|
||||||
|
member: props.event.member,
|
||||||
|
course: props.event.course,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
for (const key in data) {
|
||||||
|
if (key in certificate) certificate[key] = data[key]
|
||||||
|
certificate.name = data.name
|
||||||
|
showCertification.value = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
certificate.template = defaultTemplate.data.value
|
||||||
|
},
|
||||||
|
auto: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveCertificate = () => {
|
||||||
|
certificateResource.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(__('Success'), __('Certificate saved successfully'), 'check')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(show, () => {
|
||||||
|
if (show.value) {
|
||||||
|
evaluation.rating = 0
|
||||||
|
evaluation.status = 'Pending'
|
||||||
|
evaluation.summary = ''
|
||||||
|
evaluationDetails.reload()
|
||||||
|
|
||||||
|
certificate.published = true
|
||||||
|
certificate.issue_date = dayjs().format('YYYY-MM-DD')
|
||||||
|
certificate.expiry_date = null
|
||||||
|
certificate.template = null
|
||||||
|
certificate.name = null
|
||||||
|
certificateDetails.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const openCertificate = (certificate) => {
|
||||||
|
window.open(
|
||||||
|
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||||
|
certificate.name
|
||||||
|
}&format=${encodeURIComponent(certificate.template)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 'Pending',
|
||||||
|
label: __('Pending'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'In Progress',
|
||||||
|
label: __('In Progress'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Pass',
|
||||||
|
label: __('Pass'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Fail',
|
||||||
|
label: __('Fail'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = computed(() => {
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
label: __('Evaluation'),
|
||||||
|
icon: ClipboardList,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (showCertification.value) {
|
||||||
|
tabsArray.push({
|
||||||
|
label: __('Certification'),
|
||||||
|
icon: GraduationCap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsArray
|
||||||
|
})
|
||||||
|
</script>
|
||||||
34
frontend/src/components/Modals/ExplanationVideos.vue
Normal file
34
frontend/src/components/Modals/ExplanationVideos.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
size: '4xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="p-4">
|
||||||
|
<VideoBlock :file="file" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Dialog } from 'frappe-ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import VideoBlock from '@/components/VideoBlock.vue'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: [String, null],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const file = computed(() => {
|
||||||
|
if (props.type == 'youtube') return '/Youtube.mp4'
|
||||||
|
if (props.type == 'quiz') return '/Quiz.mp4'
|
||||||
|
if (props.type == 'upload') return '/Upload.mp4'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -212,7 +212,7 @@ const questionCreation = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const submitQuestion = (close) => {
|
const submitQuestion = (close) => {
|
||||||
if (questionData.data?.name) updateQuestion(close)
|
if (props.questionDetail?.question) updateQuestion(close)
|
||||||
else addQuestion(close)
|
else addQuestion(close)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ const addQuestion = (close) => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast(__('Error'), __(err.message?.[0] || err), 'x')
|
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -259,7 +259,7 @@ const addQuestionRow = (question, close) => {
|
|||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast(__('Error'), __(err.message?.[0] || err), 'x')
|
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -312,13 +312,12 @@ const updateQuestion = (close) => {
|
|||||||
quiz.value.reload()
|
quiz.value.reload()
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
onError(err) {
|
|
||||||
showToast(__('Error'), __(err.message?.[0] || err), 'x')
|
|
||||||
close()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="show" :options="{ size: '6xl' }">
|
<Dialog v-model="show" :options="{ size: '4xl' }">
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||||
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
||||||
@@ -28,10 +28,21 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col overflow-y-auto">
|
<div
|
||||||
|
v-if="activeTab && data.doc"
|
||||||
|
class="flex flex-1 flex-col px-10 pt-8"
|
||||||
|
>
|
||||||
|
<Members
|
||||||
|
v-if="activeTab.label === 'Members'"
|
||||||
|
:label="activeTab.label"
|
||||||
|
:description="activeTab.description"
|
||||||
|
v-model:show="show"
|
||||||
|
/>
|
||||||
<SettingDetails
|
<SettingDetails
|
||||||
v-if="activeTab && data.doc"
|
v-else
|
||||||
:fields="activeTab.fields"
|
:fields="activeTab.fields"
|
||||||
|
:label="activeTab.label"
|
||||||
|
:description="activeTab.description"
|
||||||
:data="data"
|
:data="data"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,6 +55,7 @@ import { Dialog, createDocumentResource } from 'frappe-ui'
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import SettingDetails from '../SettingDetails.vue'
|
import SettingDetails from '../SettingDetails.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
|
import Members from '@/components/Members.vue'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const doctype = ref('LMS Settings')
|
const doctype = ref('LMS Settings')
|
||||||
@@ -63,9 +75,16 @@ const tabs = computed(() => {
|
|||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Members',
|
||||||
|
description: 'Manage the members of your learning system',
|
||||||
|
icon: 'UserRoundPlus',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Payment Gateway',
|
label: 'Payment Gateway',
|
||||||
icon: 'DollarSign',
|
icon: 'DollarSign',
|
||||||
|
description:
|
||||||
|
'Configure the payment gateway and other payment related settings',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Razorpay Key',
|
label: 'Razorpay Key',
|
||||||
@@ -97,7 +116,7 @@ const tabs = computed(() => {
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Apply rounding on equivalent amount',
|
label: 'Apply rounding on equivalent',
|
||||||
name: 'apply_rounding',
|
name: 'apply_rounding',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
@@ -105,62 +124,6 @@ const tabs = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Settings',
|
|
||||||
hideLabel: true,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Signup',
|
|
||||||
icon: 'LogIn',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: 'Show terms of use on signup page',
|
|
||||||
name: 'terms_of_use',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Terms of Use Page',
|
|
||||||
name: 'terms_page',
|
|
||||||
type: 'Link',
|
|
||||||
doctype: 'Web Page',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Ask user category during signup',
|
|
||||||
name: 'user_category',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Column Break',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Show privacy policy on signup page',
|
|
||||||
name: 'privacy_policy',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Privacy Policy Page',
|
|
||||||
name: 'privacy_policy_page',
|
|
||||||
type: 'Link',
|
|
||||||
doctype: 'Web Page',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Column Break',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Show cookie policy on signup page',
|
|
||||||
name: 'cookie_policy',
|
|
||||||
type: 'checkbox',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Cookie Policy Page',
|
|
||||||
name: 'cookie_policy_page',
|
|
||||||
type: 'Link',
|
|
||||||
doctype: 'Web Page',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
@@ -168,6 +131,7 @@ const tabs = computed(() => {
|
|||||||
{
|
{
|
||||||
label: 'Sidebar',
|
label: 'Sidebar',
|
||||||
icon: 'PanelLeftIcon',
|
icon: 'PanelLeftIcon',
|
||||||
|
description: 'Choose the items you want to show in the sidebar',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Courses',
|
label: 'Courses',
|
||||||
@@ -213,6 +177,7 @@ const tabs = computed(() => {
|
|||||||
{
|
{
|
||||||
label: 'Email Templates',
|
label: 'Email Templates',
|
||||||
icon: 'MailPlus',
|
icon: 'MailPlus',
|
||||||
|
description: 'Create email templates with the content you want',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Batch Confirmation Template',
|
label: 'Batch Confirmation Template',
|
||||||
@@ -236,17 +201,61 @@ const tabs = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
/* {
|
|
||||||
label: 'Settings',
|
|
||||||
hideLabel: true,
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
label: 'Members',
|
label: 'Settings',
|
||||||
icon: "UserRoundPlus",
|
hideLabel: true,
|
||||||
component: markRaw(MemberSettings),
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Signup',
|
||||||
|
icon: 'LogIn',
|
||||||
|
description:
|
||||||
|
'Customize the signup page to inform users about your terms and policies',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Show terms of use on signup',
|
||||||
|
name: 'terms_of_use',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Terms of Use Page',
|
||||||
|
name: 'terms_page',
|
||||||
|
type: 'Link',
|
||||||
|
doctype: 'Web Page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Show privacy policy on signup',
|
||||||
|
name: 'privacy_policy',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Privacy Policy Page',
|
||||||
|
name: 'privacy_policy_page',
|
||||||
|
type: 'Link',
|
||||||
|
doctype: 'Web Page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Column Break',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Show cookie policy on signup',
|
||||||
|
name: 'cookie_policy',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cookie Policy Page',
|
||||||
|
name: 'cookie_policy_page',
|
||||||
|
type: 'Link',
|
||||||
|
doctype: 'Web Page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ask user category during signup',
|
||||||
|
name: 'user_category',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
}, */
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return _tabs.map((tab) => {
|
return _tabs.map((tab) => {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Popover transition="default">
|
|
||||||
<template #target="{ isOpen, togglePopover }" class="flex w-full">
|
|
||||||
<slot v-bind="{ isOpen, togglePopover }"></slot>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div
|
|
||||||
class="absolute left-0 mt-3 w-[35rem] max-w-lg -translate-x-1/2 transform rounded-lg bg-white px-4 sm:px-0 lg:max-w-3xl"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="overflow-hidden rounded-lg p-3 shadow-2xl ring-1 ring-black ring-opacity-5"
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
width="100%"
|
|
||||||
controlsList="nodownload"
|
|
||||||
oncontextmenu="return false;"
|
|
||||||
class="rounded-sm"
|
|
||||||
>
|
|
||||||
<source src="/Youtube.mov" type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { Popover } from 'frappe-ui'
|
|
||||||
</script>
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{{ __('Please login to access this page.') }}
|
{{ __('Please login to access this page.') }}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="solid" @click="redirectToLogin()" class="mt-2">
|
<Button @click="redirectToLogin()" class="mt-4">
|
||||||
{{ __('Login') }}
|
{{ __('Login') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-gray-900 font-semibold mt-2"
|
class="text-gray-900 font-semibold mt-2 leading-5"
|
||||||
v-html="questionDetails.data.question"
|
v-html="questionDetails.data.question"
|
||||||
></div>
|
></div>
|
||||||
<div v-if="questionDetails.data.type == 'Choices'" v-for="index in 4">
|
<div v-if="questionDetails.data.type == 'Choices'" v-for="index in 4">
|
||||||
@@ -439,7 +439,7 @@ const checkAnswer = () => {
|
|||||||
const addToLocalStorage = () => {
|
const addToLocalStorage = () => {
|
||||||
let quizData = JSON.parse(localStorage.getItem(quiz.data.title))
|
let quizData = JSON.parse(localStorage.getItem(quiz.data.title))
|
||||||
let questionData = {
|
let questionData = {
|
||||||
question_index: activeQuestion.value,
|
question_name: currentQuestion.value,
|
||||||
answer: getAnswers().join(),
|
answer: getAnswers().join(),
|
||||||
is_correct: showAnswers.filter((answer) => {
|
is_correct: showAnswers.filter((answer) => {
|
||||||
return answer != undefined
|
return answer != undefined
|
||||||
|
|||||||
58
frontend/src/components/QuizPlugin.vue
Normal file
58
frontend/src/components/QuizPlugin.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
size: 'xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
{{ __('Add a quiz to your lesson') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
v-model="quiz"
|
||||||
|
doctype="LMS Quiz"
|
||||||
|
:label="__('Select a quiz')"
|
||||||
|
:onCreate="(value, close) => redirectToQuizForm()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<Button variant="solid" @click="addQuiz()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Dialog, Button } from 'frappe-ui'
|
||||||
|
import { onMounted, ref, nextTick } from 'vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
const quiz = ref(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onQuizAddition: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
show.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const addQuiz = () => {
|
||||||
|
props.onQuizAddition(quiz.value)
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectToQuizForm = () => {
|
||||||
|
window.open('/lms/quizzes/new', '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col justify-between h-full p-8">
|
<div class="flex flex-col justify-between h-full">
|
||||||
<div class="flex space-x-10">
|
<div>
|
||||||
|
<div class="font-semibold mb-1">
|
||||||
|
{{ __(label) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
{{ __(description) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between my-5">
|
||||||
<div v-for="(column, index) in columns" :key="index">
|
<div v-for="(column, index) in columns" :key="index">
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-5 w-72">
|
||||||
<div v-for="field in column" :class="width">
|
<div v-for="field in column">
|
||||||
<Link
|
<Link
|
||||||
v-if="field.type == 'Link'"
|
v-if="field.type == 'Link'"
|
||||||
v-model="field.value"
|
v-model="field.value"
|
||||||
@@ -31,11 +39,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { FormControl, Button } from 'frappe-ui'
|
import { FormControl, Button } from 'frappe-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
let width = ref('w-full')
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -45,6 +51,13 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
@@ -71,12 +84,6 @@ const columns = computed(() => {
|
|||||||
cols.push(currentColumn)
|
cols.push(currentColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cols.length == 3) {
|
|
||||||
width.value = 'w-64'
|
|
||||||
} else {
|
|
||||||
width.value = 'w-96'
|
|
||||||
}
|
|
||||||
|
|
||||||
return cols
|
return cols
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
53
frontend/src/components/UploadPlugin.vue
Normal file
53
frontend/src/components/UploadPlugin.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<FileUploader
|
||||||
|
:fileTypes="['image/*', 'video/*', 'audio/*', '.pdf']"
|
||||||
|
:validateFile="validateFile"
|
||||||
|
@success="(data) => addFile(data)"
|
||||||
|
ref="fileUploader"
|
||||||
|
class="hide"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { FileUploader } from 'frappe-ui'
|
||||||
|
import { onMounted, ref, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const fileUploader = ref(null)
|
||||||
|
const emit = defineEmits(['fileUploaded'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onFileUploaded: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
const fileInput = fileUploader.value.$el.querySelector('input[type="file"]')
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addFile = (file) => {
|
||||||
|
props.onFileUploaded({
|
||||||
|
file_url: file.file_url,
|
||||||
|
file_type: file.file_type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateFile = (file) => {
|
||||||
|
let extension = file.name.split('.').pop().toLowerCase()
|
||||||
|
if (!['jpg', 'jpeg', 'png', 'mp4', 'mov', 'mp3', 'pdf'].includes(extension)) {
|
||||||
|
return 'Only image and video files are allowed.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVideo = (type) => {
|
||||||
|
return ['mov', 'mp4', 'avi', 'mkv', 'webm'].includes(type.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAudio = (type) => {
|
||||||
|
return ['mp3', 'wav', 'ogg'].includes(type.toLowerCase())
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
|
import Apps from '@/components/Apps.vue'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LogIn,
|
LogIn,
|
||||||
@@ -77,7 +78,7 @@ import {
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { convertToTitleCase } from '../utils'
|
import { convertToTitleCase } from '../utils'
|
||||||
import { usersStore } from '@/stores/user'
|
import { usersStore } from '@/stores/user'
|
||||||
import { ref } from 'vue'
|
import { ref, markRaw } from 'vue'
|
||||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
import SettingsModal from '@/components/Modals/Settings.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -105,11 +106,7 @@ const userDropdownOptions = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ArrowRightLeft,
|
component: markRaw(Apps),
|
||||||
label: 'Switch to Desk',
|
|
||||||
onClick: () => {
|
|
||||||
window.location.href = '/app'
|
|
||||||
},
|
|
||||||
condition: () => {
|
condition: () => {
|
||||||
let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
|
let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
|
||||||
let system_user = cookies.get('system_user')
|
let system_user = cookies.get('system_user')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@timeupdate="updateTime"
|
@timeupdate="updateTime"
|
||||||
@ended="videoEnded"
|
@ended="videoEnded"
|
||||||
class="rounded-lg border border-gray-100"
|
class="rounded-lg border border-gray-100"
|
||||||
|
ref="videoRef"
|
||||||
>
|
>
|
||||||
<source :src="fileURL" :type="type" />
|
<source :src="fileURL" :type="type" />
|
||||||
</video>
|
</video>
|
||||||
@@ -71,7 +72,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
videoRef.value = document.querySelector('video')
|
|
||||||
videoRef.value.onloadedmetadata = () => {
|
videoRef.value.onloadedmetadata = () => {
|
||||||
duration.value = videoRef.value.duration
|
duration.value = videoRef.value.duration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,9 @@
|
|||||||
<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">
|
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl v-model="batch.title" :label="__('Title')" />
|
||||||
v-model="batch.title"
|
|
||||||
:label="__('Title')"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ const validateMandatoryFields = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (course.paid_course && (!course.course_price || !course.currency)) {
|
if (course.paid_course && (!course.course_price || !course.currency)) {
|
||||||
return 'Course price and currency are mandatory for paid courses'
|
return __('Course price and currency are mandatory for paid courses')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +436,7 @@ watch(
|
|||||||
const validateFile = (file) => {
|
const validateFile = (file) => {
|
||||||
let extension = file.name.split('.').pop().toLowerCase()
|
let extension = file.name.split('.').pop().toLowerCase()
|
||||||
if (!['jpg', 'jpeg', 'png', 'webp'].includes(extension)) {
|
if (!['jpg', 'jpeg', 'png', 'webp'].includes(extension)) {
|
||||||
return 'Only image file is allowed.'
|
return __('Only image file is allowed.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
lesson.data.instructor_content?.blocks?.length &&
|
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
|
||||||
allowInstructorContent()
|
allowInstructorContent()
|
||||||
"
|
"
|
||||||
class="bg-gray-100 p-3 rounded-md mt-6"
|
class="bg-gray-100 p-3 rounded-md mt-6"
|
||||||
@@ -244,7 +244,7 @@ const lesson = createResource({
|
|||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
lessonProgress.value = data.membership?.progress
|
lessonProgress.value = data.membership?.progress
|
||||||
if (data.content) editor.value = renderEditor('editor', data.content)
|
if (data.content) editor.value = renderEditor('editor', data.content)
|
||||||
if (data.instructor_content?.blocks?.length)
|
if (JSON.parse(data.instructor_content)?.blocks?.length > 1)
|
||||||
instructorEditor.value = renderEditor(
|
instructorEditor.value = renderEditor(
|
||||||
'instructor-content',
|
'instructor-content',
|
||||||
data.instructor_content
|
data.instructor_content
|
||||||
@@ -448,6 +448,10 @@ updateDocumentTitle(pageMeta)
|
|||||||
max-width: unset;
|
max-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codex-editor__redactor {
|
||||||
|
padding-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.codeBoxHolder {
|
.codeBoxHolder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -537,4 +541,13 @@ updateDocumentTitle(pageMeta)
|
|||||||
color: #383a42;
|
color: #383a42;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codeBoxTextArea {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border-top: 3px solid theme('colors.gray.700');
|
||||||
|
border-bottom: 3px solid theme('colors.gray.700');
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="sticky top-0 p-5">
|
<div class="sticky top-0 p-5">
|
||||||
<LessonPlugins :editor="editor" :notesEditor="instructorEditor" />
|
<LessonHelp />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@ import {
|
|||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import EditorJS from '@editorjs/editorjs'
|
import EditorJS from '@editorjs/editorjs'
|
||||||
import LessonPlugins from '@/components/LessonPlugins.vue'
|
import LessonHelp from '@/components/LessonHelp.vue'
|
||||||
import { ChevronRight } from 'lucide-vue-next'
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
import { updateDocumentTitle, createToast, getEditorTools } from '@/utils'
|
import { updateDocumentTitle, createToast, getEditorTools } from '@/utils'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
@@ -117,7 +117,7 @@ onMounted(() => {
|
|||||||
const renderEditor = (holder) => {
|
const renderEditor = (holder) => {
|
||||||
return new EditorJS({
|
return new EditorJS({
|
||||||
holder: holder,
|
holder: holder,
|
||||||
tools: getEditorTools(),
|
tools: getEditorTools(true),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,9 @@ const lessonDetails = createResource({
|
|||||||
Object.keys(data.lesson).forEach((key) => {
|
Object.keys(data.lesson).forEach((key) => {
|
||||||
lesson[key] = data.lesson[key]
|
lesson[key] = data.lesson[key]
|
||||||
})
|
})
|
||||||
lesson.include_in_preview = data.include_in_preview ? true : false
|
lesson.include_in_preview = data?.lesson?.include_in_preview
|
||||||
|
? true
|
||||||
|
: false
|
||||||
addLessonContent(data)
|
addLessonContent(data)
|
||||||
addInstructorNotes(data)
|
addInstructorNotes(data)
|
||||||
enableAutoSave()
|
enableAutoSave()
|
||||||
@@ -180,7 +182,7 @@ const addInstructorNotes = (data) => {
|
|||||||
const enableAutoSave = () => {
|
const enableAutoSave = () => {
|
||||||
autoSaveInterval = setInterval(() => {
|
autoSaveInterval = setInterval(() => {
|
||||||
saveLesson()
|
saveLesson()
|
||||||
}, 5000)
|
}, 10000)
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -423,7 +425,7 @@ const breadcrumbs = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: lessonDetails.data?.course_title,
|
label: lessonDetails.data?.course_title,
|
||||||
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
route: { name: 'CourseForm', params: { courseName: props.courseName } },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -473,6 +475,10 @@ updateDocumentTitle(pageMeta)
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codex-editor--narrow .ce-toolbar__actions {
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.ce-toolbar__content {
|
.ce-toolbar__content {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
@@ -545,10 +551,6 @@ updateDocumentTitle(pageMeta)
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeBoxSelectItem:hover {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeBoxSelectedItem {
|
.codeBoxSelectedItem {
|
||||||
background-color: lightblue !important;
|
background-color: lightblue !important;
|
||||||
}
|
}
|
||||||
@@ -566,4 +568,17 @@ updateDocumentTitle(pageMeta)
|
|||||||
color: #383a42;
|
color: #383a42;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codeBoxTextArea {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(pre):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
||||||
|
overflow-x: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border-top: 3px solid theme('colors.gray.700');
|
||||||
|
border-bottom: 3px solid theme('colors.gray.700');
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const coverImage = createResource({
|
|||||||
|
|
||||||
const setActiveTab = () => {
|
const setActiveTab = () => {
|
||||||
let fragments = route.path.split('/')
|
let fragments = route.path.split('/')
|
||||||
let sections = ['certificates', 'roles', 'evaluations']
|
let sections = ['certificates', 'roles', 'slots', 'schedule']
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (fragments.includes(section)) {
|
if (fragments.includes(section)) {
|
||||||
activeTab.value = convertToTitleCase(section)
|
activeTab.value = convertToTitleCase(section)
|
||||||
@@ -161,7 +161,8 @@ watchEffect(() => {
|
|||||||
About: { name: 'ProfileAbout' },
|
About: { name: 'ProfileAbout' },
|
||||||
Certificates: { name: 'ProfileCertificates' },
|
Certificates: { name: 'ProfileCertificates' },
|
||||||
Roles: { name: 'ProfileRoles' },
|
Roles: { name: 'ProfileRoles' },
|
||||||
Evaluations: { name: 'ProfileEvaluator' },
|
Slots: { name: 'ProfileEvaluator' },
|
||||||
|
Schedule: { name: 'ProfileEvaluationSchedule' },
|
||||||
}[activeTab.value]
|
}[activeTab.value]
|
||||||
router.push(route)
|
router.push(route)
|
||||||
}
|
}
|
||||||
@@ -185,8 +186,13 @@ const isSessionUser = () => {
|
|||||||
const getTabButtons = () => {
|
const getTabButtons = () => {
|
||||||
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
|
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
|
||||||
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
|
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
|
||||||
if (isSessionUser() && $user.data?.is_evaluator)
|
if (
|
||||||
buttons.push({ label: 'Evaluations' })
|
isSessionUser() &&
|
||||||
|
($user.data?.is_evaluator || $user.data?.is_moderator)
|
||||||
|
) {
|
||||||
|
buttons.push({ label: 'Slots' })
|
||||||
|
buttons.push({ label: 'Schedule' })
|
||||||
|
}
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
}
|
}
|
||||||
|
|||||||
102
frontend/src/pages/ProfileEvaluationSchedule.vue
Normal file
102
frontend/src/pages/ProfileEvaluationSchedule.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-7 mb-20">
|
||||||
|
<div class="flex h-screen flex-col overflow-hidden">
|
||||||
|
<Calendar
|
||||||
|
v-if="evaluations.data?.length"
|
||||||
|
:config="{
|
||||||
|
defaultMode: 'Month',
|
||||||
|
disableModes: ['Day', 'Week'],
|
||||||
|
redundantCellHeight: 100,
|
||||||
|
enableShortcuts: false,
|
||||||
|
}"
|
||||||
|
:events="evaluations.data"
|
||||||
|
@click="(event) => openEvent(event)"
|
||||||
|
>
|
||||||
|
<template #header="{ currentMonthYear, decrement, increment }">
|
||||||
|
<div class="mb-2 flex justify-between">
|
||||||
|
<span class="text-lg font-semibold">
|
||||||
|
{{ currentMonthYear }}
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-x-1">
|
||||||
|
<Button
|
||||||
|
@click="decrement()"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-4 w-4"
|
||||||
|
icon="chevron-left"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
@click="increment()"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-4 w-4"
|
||||||
|
icon="chevron-right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Calendar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Event v-model="showEvent" :event="currentEvent" />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Calendar, createListResource, Button } from 'frappe-ui'
|
||||||
|
import { inject, ref } from 'vue'
|
||||||
|
import Event from '@/components/Modals/Event.vue'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
const currentEvent = ref(null)
|
||||||
|
const showEvent = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
profile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const evaluations = createListResource({
|
||||||
|
doctype: 'LMS Certificate Request',
|
||||||
|
filters: {
|
||||||
|
evaluator: user.data?.name,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'member_name',
|
||||||
|
'member',
|
||||||
|
'course',
|
||||||
|
'course_title',
|
||||||
|
'batch_name',
|
||||||
|
'batch_title',
|
||||||
|
'evaluator',
|
||||||
|
'evaluator_name',
|
||||||
|
'date',
|
||||||
|
'start_time',
|
||||||
|
'end_time',
|
||||||
|
'google_meet_link',
|
||||||
|
],
|
||||||
|
auto: true,
|
||||||
|
orderBy: 'creation desc',
|
||||||
|
limit: 100,
|
||||||
|
cache: ['schedule', user.data?.name],
|
||||||
|
transform(data) {
|
||||||
|
return data.map((d) => {
|
||||||
|
let mappedData = Object.assign({}, d)
|
||||||
|
|
||||||
|
mappedData.title = `${d.member_name}'s Evaluation`
|
||||||
|
mappedData.participant = d.member_name
|
||||||
|
mappedData.id = d.name
|
||||||
|
mappedData.venue = d.google_meet_link
|
||||||
|
mappedData.fromDate = `${d.date} ${d.start_time}`
|
||||||
|
mappedData.toDate = `${d.date} ${d.end_time}`
|
||||||
|
mappedData.color = 'green'
|
||||||
|
|
||||||
|
return mappedData
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const openEvent = (event) => {
|
||||||
|
currentEvent.value = event.calendarEvent
|
||||||
|
showEvent.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div v-if="quizDetails.data?.name">
|
<div v-if="quizDetails.data?.name">
|
||||||
<div class="grid grid-cols-3 gap-5 mt-2 mb-8">
|
<div class="grid grid-cols-3 gap-5 mt-4 mb-8">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="quiz.max_attempts"
|
v-model="quiz.max_attempts"
|
||||||
:label="__('Maximun Attempts')"
|
:label="__('Maximun Attempts')"
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="deleteQuizzes(selections, unselectAll)"
|
@click="deleteQuestions(selections, unselectAll)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -174,7 +174,7 @@ import {
|
|||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||||
import Question from '@/components/Modals/Question.vue'
|
import Question from '@/components/Modals/Question.vue'
|
||||||
import { showToast } from '../utils'
|
import { showToast, updateDocumentTitle } from '@/utils'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const showQuestionModal = ref(false)
|
const showQuestionModal = ref(false)
|
||||||
@@ -306,7 +306,7 @@ const createQuiz = () => {
|
|||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
showToast(__('Success'), __('Quiz created successfully'), 'check')
|
showToast(__('Success'), __('Quiz created successfully'), 'check')
|
||||||
router.push({
|
router.push({
|
||||||
name: 'QuizCreation',
|
name: 'QuizForm',
|
||||||
params: { quizID: data.name },
|
params: { quizID: data.name },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -375,24 +375,29 @@ const openQuestionModal = (question = null) => {
|
|||||||
showQuestionModal.value = true
|
showQuestionModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteQuiz = createResource({
|
const deleteQuestionResource = createResource({
|
||||||
url: 'frappe.client.delete',
|
url: 'lms.lms.api.delete_documents',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: 'LMS Quiz Question',
|
doctype: 'LMS Quiz Question',
|
||||||
name: values.quiz,
|
documents: values.questions,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteQuizzes = (selections, unselectAll) => {
|
const deleteQuestions = (selections, unselectAll) => {
|
||||||
selections.forEach(async (quiz) => {
|
deleteQuestionResource.submit(
|
||||||
deleteQuiz.submit({ quiz })
|
{
|
||||||
})
|
questions: Array.from(selections),
|
||||||
setTimeout(() => {
|
},
|
||||||
quizDetails.reload()
|
{
|
||||||
unselectAll()
|
onSuccess() {
|
||||||
}, 500)
|
showToast(__('Success'), __('Questions deleted successfully'), 'check')
|
||||||
|
quizDetails.reload()
|
||||||
|
unselectAll()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
@@ -410,9 +415,18 @@ const breadcrumbs = computed(() => {
|
|||||||
})
|
})
|
||||||
} */
|
} */
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
label: props.quizID == 'new' ? 'New Quiz' : quizDetails.data?.title,
|
label: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title,
|
||||||
route: { name: 'QuizCreation', params: { quizID: props.quizID } },
|
route: { name: 'QuizForm', params: { quizID: props.quizID } },
|
||||||
})
|
})
|
||||||
return crumbs
|
return crumbs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pageMeta = computed(() => {
|
||||||
|
return {
|
||||||
|
title: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title,
|
||||||
|
description: __('Form to create and edit quizzes'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
updateDocumentTitle(pageMeta)
|
||||||
</script>
|
</script>
|
||||||
48
frontend/src/pages/QuizSubmission.vue
Normal file
48
frontend/src/pages/QuizSubmission.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
</header>
|
||||||
|
<div class="w-1/2 mx-auto py-10">
|
||||||
|
<Quiz :quizName="quizID" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Quiz from '@/components/Quiz.vue'
|
||||||
|
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||||
|
import { computed, inject, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!user.data) {
|
||||||
|
router.push({ name: 'Courses' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
quizID: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = createResource({
|
||||||
|
url: 'frappe.client.get_value',
|
||||||
|
params: {
|
||||||
|
doctype: 'LMS Quiz',
|
||||||
|
fieldname: 'title',
|
||||||
|
filters: {
|
||||||
|
name: props.quizID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
return [{ label: __('Quiz Submission') }, { label: title.data?.title }]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'QuizCreation',
|
name: 'QuizForm',
|
||||||
params: {
|
params: {
|
||||||
quizID: 'new',
|
quizID: 'new',
|
||||||
},
|
},
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-for="row in quizzes.data"
|
v-for="row in quizzes.data"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'QuizCreation',
|
name: 'QuizForm',
|
||||||
params: {
|
params: {
|
||||||
quizID: row.name,
|
quizID: row.name,
|
||||||
},
|
},
|
||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { computed, inject, onMounted } from 'vue'
|
import { computed, inject, onMounted } from 'vue'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { Plus } from 'lucide-vue-next'
|
||||||
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -123,4 +124,13 @@ const breadcrumbs = computed(() => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pageMeta = computed(() => {
|
||||||
|
return {
|
||||||
|
title: __('Quizzes'),
|
||||||
|
description: __('List of quizzes'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
updateDocumentTitle(pageMeta)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -79,9 +79,15 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ProfileEvaluator',
|
name: 'ProfileEvaluator',
|
||||||
path: 'evaluations',
|
path: 'slots',
|
||||||
component: () => import('@/pages/ProfileEvaluator.vue'),
|
component: () => import('@/pages/ProfileEvaluator.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ProfileEvaluationSchedule',
|
||||||
|
path: 'schedule',
|
||||||
|
component: () =>
|
||||||
|
import('@/pages/ProfileEvaluationSchedule.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -148,8 +154,14 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/quizzes/:quizID',
|
path: '/quizzes/:quizID',
|
||||||
name: 'QuizCreation',
|
name: 'QuizForm',
|
||||||
component: () => import('@/pages/QuizCreation.vue'),
|
component: () => import('@/pages/QuizForm.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/quiz/:quizID',
|
||||||
|
name: 'Quiz',
|
||||||
|
component: () => import('@/pages/QuizSubmission.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export class CodeBox {
|
|||||||
|
|
||||||
static get toolbox() {
|
static get toolbox() {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
render: () => h(Code, { size: 24, strokeWidth: 2, color: 'black' }),
|
render: () => h(Code, { size: 18, strokeWidth: 1.5, color: 'black' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import Embed from '@editorjs/embed'
|
|
||||||
import VideoBlock from '@/components/VideoBlock.vue'
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
|
|
||||||
export class CustomEmbed extends Embed {
|
|
||||||
render() {
|
|
||||||
const container = super.render()
|
|
||||||
const { service, source, embed } = this.data
|
|
||||||
|
|
||||||
if (service === 'youtube' || service === 'vimeo') {
|
|
||||||
// Remove the iframe or existing embed content
|
|
||||||
container.innerHTML = ''
|
|
||||||
|
|
||||||
// Create a placeholder element for Vue component
|
|
||||||
const vueContainer = document.createElement('div')
|
|
||||||
vueContainer.setAttribute('data-service', service)
|
|
||||||
vueContainer.setAttribute('data-video-id', this.data.source)
|
|
||||||
|
|
||||||
// Append the Vue placeholder
|
|
||||||
container.appendChild(vueContainer)
|
|
||||||
console.log(source)
|
|
||||||
// Mount the Vue component (using a global Vue instance)
|
|
||||||
const app = createApp(VideoBlock, {
|
|
||||||
file: source,
|
|
||||||
type: 'video/youtube',
|
|
||||||
})
|
|
||||||
app.mount(vueContainer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -149,9 +149,9 @@ export function getEditorTools() {
|
|||||||
class: CodeBox,
|
class: CodeBox,
|
||||||
config: {
|
config: {
|
||||||
themeURL:
|
themeURL:
|
||||||
'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/dracula.min.css', // Optional
|
'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/atom-one-dark.min.css',
|
||||||
themeName: 'atom-one-dark', // Optional
|
themeName: 'atom-one-dark',
|
||||||
useDefaultTheme: 'dark', // Optional. This also determines the background color of the language select drop-down
|
useDefaultTheme: 'dark',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
@@ -233,9 +233,9 @@ export function getEditorTools() {
|
|||||||
},
|
},
|
||||||
github: true,
|
github: true,
|
||||||
slides: {
|
slides: {
|
||||||
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
|
regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/,
|
||||||
embedUrl:
|
embedUrl:
|
||||||
'https://docs.google.com/presentation/d/e/<%= remote_id %>/embed',
|
'https://docs.google.com/presentation/d/<%= remote_id %>/embed',
|
||||||
html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>",
|
html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>",
|
||||||
},
|
},
|
||||||
drive: {
|
drive: {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import QuizBlock from '@/components/QuizBlock.vue'
|
import QuizBlock from '@/components/QuizBlock.vue'
|
||||||
import { createApp } from 'vue'
|
import QuizPlugin from '@/components/QuizPlugin.vue'
|
||||||
|
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'
|
||||||
|
|
||||||
export class Quiz {
|
export class Quiz {
|
||||||
constructor({ data, api, readOnly }) {
|
constructor({ data, api, readOnly }) {
|
||||||
@@ -9,17 +11,31 @@ export class Quiz {
|
|||||||
this.readOnly = readOnly
|
this.readOnly = readOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get toolbox() {
|
||||||
|
const app = createApp({
|
||||||
|
render: () =>
|
||||||
|
h(CircleHelp, { size: 18, strokeWidth: 1.5, color: 'black' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const div = document.createElement('div')
|
||||||
|
app.mount(div)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: __('Quiz'),
|
||||||
|
icon: div.innerHTML,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get isReadOnlySupported() {
|
static get isReadOnlySupported() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.wrapper = document.createElement('div')
|
this.wrapper = document.createElement('div')
|
||||||
if (this.data) {
|
if (Object.keys(this.data).length) {
|
||||||
let renderedQuiz = this.renderQuiz(this.data.quiz)
|
this.renderQuiz(this.data.quiz)
|
||||||
if (!this.readOnly) {
|
} else {
|
||||||
this.wrapper.innerHTML = renderedQuiz
|
this.renderQuizModal()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this.wrapper
|
return this.wrapper
|
||||||
}
|
}
|
||||||
@@ -27,7 +43,7 @@ export class Quiz {
|
|||||||
renderQuiz(quiz) {
|
renderQuiz(quiz) {
|
||||||
if (this.readOnly) {
|
if (this.readOnly) {
|
||||||
const app = createApp(QuizBlock, {
|
const app = createApp(QuizBlock, {
|
||||||
quiz: quiz, // Pass quiz content as prop
|
quiz: quiz,
|
||||||
})
|
})
|
||||||
app.use(translationPlugin)
|
app.use(translationPlugin)
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
@@ -35,11 +51,23 @@ export class Quiz {
|
|||||||
app.mount(this.wrapper)
|
app.mount(this.wrapper)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return `<div class='border rounded-md p-10 text-center mb-2'>
|
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center mb-2'>
|
||||||
<span class="font-medium">
|
<span class="font-medium">
|
||||||
Quiz: ${quiz}
|
Quiz: ${quiz}
|
||||||
</span>
|
</span>
|
||||||
</div>`
|
</div>`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderQuizModal() {
|
||||||
|
const app = createApp(QuizPlugin, {
|
||||||
|
onQuizAddition: (quiz) => {
|
||||||
|
this.data.quiz = quiz
|
||||||
|
this.renderQuiz(quiz)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app.use(translationPlugin)
|
||||||
|
app.mount(this.wrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
save(blockContent) {
|
save(blockContent) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import AudioBlock from '@/components/AudioBlock.vue'
|
import AudioBlock from '@/components/AudioBlock.vue'
|
||||||
import VideoBlock from '@/components/VideoBlock.vue'
|
import VideoBlock from '@/components/VideoBlock.vue'
|
||||||
import { createApp } from 'vue'
|
import UploadPlugin from '@/components/UploadPlugin.vue'
|
||||||
|
import { h, createApp } from 'vue'
|
||||||
|
import { Upload as UploadIcon } from 'lucide-vue-next'
|
||||||
|
import translationPlugin from '../translation'
|
||||||
|
|
||||||
export class Upload {
|
export class Upload {
|
||||||
constructor({ data, api, readOnly }) {
|
constructor({ data, api, readOnly }) {
|
||||||
@@ -8,17 +11,38 @@ export class Upload {
|
|||||||
this.readOnly = readOnly
|
this.readOnly = readOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get toolbox() {
|
||||||
|
const app = createApp({
|
||||||
|
render: () =>
|
||||||
|
h(UploadIcon, { size: 18, strokeWidth: 1.5, color: 'black' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const div = document.createElement('div')
|
||||||
|
app.mount(div)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'Upload',
|
||||||
|
icon: div.innerHTML,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get isReadOnlySupported() {
|
static get isReadOnlySupported() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.wrapper = document.createElement('div')
|
this.wrapper = document.createElement('div')
|
||||||
this.renderUpload(this.data)
|
|
||||||
|
if (this.data && this.data.file_url) {
|
||||||
|
this.renderFile(this.data)
|
||||||
|
} else {
|
||||||
|
this.renderFileUploader()
|
||||||
|
}
|
||||||
|
|
||||||
return this.wrapper
|
return this.wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUpload(file) {
|
renderFile(file) {
|
||||||
if (this.isVideo(file.file_type)) {
|
if (this.isVideo(file.file_type)) {
|
||||||
const app = createApp(VideoBlock, {
|
const app = createApp(VideoBlock, {
|
||||||
file: file.file_url,
|
file: file.file_url,
|
||||||
@@ -44,6 +68,25 @@ export class Upload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderFileUploader() {
|
||||||
|
const app = createApp(UploadPlugin, {
|
||||||
|
onFileUploaded: (file) => {
|
||||||
|
this.data.file_url = file.file_url
|
||||||
|
this.data.file_type = file.file_type
|
||||||
|
this.renderFile(file)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app.use(translationPlugin)
|
||||||
|
app.mount(this.wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(savedData) {
|
||||||
|
if (!savedData.file_url || !savedData.file_type) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
save(blockContent) {
|
save(blockContent) {
|
||||||
return {
|
return {
|
||||||
file_url: this.data.file_url,
|
file_url: this.data.file_url,
|
||||||
|
|||||||
2122
frontend/yarn.lock
2122
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
__version__ = "2.2.0"
|
__version__ = "2.5.0"
|
||||||
|
|||||||
18
lms/hooks.py
18
lms/hooks.py
@@ -4,9 +4,11 @@ app_name = "frappe_lms"
|
|||||||
app_title = "Frappe LMS"
|
app_title = "Frappe LMS"
|
||||||
app_publisher = "Frappe"
|
app_publisher = "Frappe"
|
||||||
app_description = "Frappe LMS App"
|
app_description = "Frappe LMS App"
|
||||||
app_icon = "octicon octicon-file-directory"
|
app_icon_url = "/assets/lms/images/lms-logo.png"
|
||||||
|
app_icon_title = "Learning"
|
||||||
|
app_icon_route = "/lms"
|
||||||
app_color = "grey"
|
app_color = "grey"
|
||||||
app_email = "school@frappe.io"
|
app_email = "jannat@frappe.io"
|
||||||
app_license = "AGPL"
|
app_license = "AGPL"
|
||||||
|
|
||||||
# Includes in <head>
|
# Includes in <head>
|
||||||
@@ -61,8 +63,6 @@ web_include_js = ["website.bundle.js"]
|
|||||||
after_install = "lms.install.after_install"
|
after_install = "lms.install.after_install"
|
||||||
after_sync = "lms.install.after_sync"
|
after_sync = "lms.install.after_sync"
|
||||||
before_uninstall = "lms.install.before_uninstall"
|
before_uninstall = "lms.install.before_uninstall"
|
||||||
|
|
||||||
|
|
||||||
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
||||||
|
|
||||||
# Desk Notifications
|
# Desk Notifications
|
||||||
@@ -231,3 +231,13 @@ 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"
|
on_session_creation = "lms.overrides.user.on_session_creation"
|
||||||
|
|
||||||
|
add_to_apps_screen = [
|
||||||
|
{
|
||||||
|
"name": "lms",
|
||||||
|
"logo": "/assets/lms/images/lms-logo.png",
|
||||||
|
"title": "Learning",
|
||||||
|
"route": "/lms",
|
||||||
|
"has_permission": "lms.lms.api.check_app_permission",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
144
lms/lms/api.py
144
lms/lms/api.py
@@ -7,6 +7,7 @@ from frappe import _
|
|||||||
from frappe.query_builder import DocType
|
from frappe.query_builder import DocType
|
||||||
from frappe.query_builder.functions import Count
|
from frappe.query_builder.functions import Count
|
||||||
from frappe.utils import time_diff, now_datetime, get_datetime
|
from frappe.utils import time_diff, now_datetime, get_datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -562,3 +563,146 @@ def get_categories(doctype, filters):
|
|||||||
categoryOptions.append({"label": category, "value": category})
|
categoryOptions.append({"label": category, "value": category})
|
||||||
|
|
||||||
return categoryOptions
|
return categoryOptions
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_members(start=0, search=""):
|
||||||
|
"""Get members for the given search term and start index.
|
||||||
|
Args: start (int): Start index for the query.
|
||||||
|
search (str): Search term to filter the results.
|
||||||
|
Returns: List of members.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
||||||
|
|
||||||
|
if search:
|
||||||
|
filters["full_name"] = ["like", f"%{search}%"]
|
||||||
|
|
||||||
|
members = frappe.get_all(
|
||||||
|
"User",
|
||||||
|
filters=filters,
|
||||||
|
fields=["name", "full_name", "user_image", "username", "last_active"],
|
||||||
|
page_length=20,
|
||||||
|
start=start,
|
||||||
|
)
|
||||||
|
|
||||||
|
for member in members:
|
||||||
|
roles = frappe.get_roles(member.name)
|
||||||
|
if "Moderator" in roles:
|
||||||
|
member.role = "Moderator"
|
||||||
|
elif "Course Creator" in roles:
|
||||||
|
member.role = "Course Creator"
|
||||||
|
elif "Batch Evaluator" in roles:
|
||||||
|
member.role = "Batch Evaluator"
|
||||||
|
elif "LMS Student" in roles:
|
||||||
|
member.role = "LMS Student"
|
||||||
|
|
||||||
|
return members
|
||||||
|
|
||||||
|
|
||||||
|
def check_app_permission():
|
||||||
|
"""Check if the user has permission to access the app."""
|
||||||
|
if frappe.session.user == "Administrator":
|
||||||
|
return True
|
||||||
|
|
||||||
|
roles = frappe.get_roles()
|
||||||
|
lms_roles = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"]
|
||||||
|
if any(role in roles for role in lms_roles):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def save_evaluation_details(
|
||||||
|
member,
|
||||||
|
course,
|
||||||
|
batch_name,
|
||||||
|
evaluator,
|
||||||
|
date,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
status,
|
||||||
|
rating,
|
||||||
|
summary,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Save evaluation details for a member against a course.
|
||||||
|
"""
|
||||||
|
evaluation = frappe.db.exists(
|
||||||
|
"LMS Certificate Evaluation", {"member": member, "course": course}
|
||||||
|
)
|
||||||
|
|
||||||
|
details = {
|
||||||
|
"date": date,
|
||||||
|
"start_time": start_time,
|
||||||
|
"end_time": end_time,
|
||||||
|
"status": status,
|
||||||
|
"rating": rating / 5,
|
||||||
|
"summary": summary,
|
||||||
|
"batch_name": batch_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if evaluation:
|
||||||
|
frappe.db.set_value("LMS Certificate Evaluation", evaluation, details)
|
||||||
|
return evaluation
|
||||||
|
else:
|
||||||
|
doc = frappe.new_doc("LMS Certificate Evaluation")
|
||||||
|
details.update(
|
||||||
|
{
|
||||||
|
"member": member,
|
||||||
|
"course": course,
|
||||||
|
"evaluator": evaluator,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.update(details)
|
||||||
|
doc.insert()
|
||||||
|
return doc.name
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def save_certificate_details(
|
||||||
|
member,
|
||||||
|
course,
|
||||||
|
batch_name,
|
||||||
|
evaluator,
|
||||||
|
issue_date,
|
||||||
|
expiry_date,
|
||||||
|
template,
|
||||||
|
published=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Save certificate details for a member against a course.
|
||||||
|
"""
|
||||||
|
certificate = frappe.db.exists("LMS Certificate", {"member": member, "course": course})
|
||||||
|
|
||||||
|
details = {
|
||||||
|
"published": published,
|
||||||
|
"issue_date": issue_date,
|
||||||
|
"expiry_date": expiry_date,
|
||||||
|
"template": template,
|
||||||
|
"batch_name": batch_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if certificate:
|
||||||
|
frappe.db.set_value("LMS Certificate", certificate, details)
|
||||||
|
return certificate
|
||||||
|
else:
|
||||||
|
doc = frappe.new_doc("LMS Certificate")
|
||||||
|
details.update(
|
||||||
|
{
|
||||||
|
"member": member,
|
||||||
|
"course": course,
|
||||||
|
"evaluator": evaluator,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.update(details)
|
||||||
|
doc.insert()
|
||||||
|
return doc.name
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def delete_documents(doctype, documents):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
|
for doc in documents:
|
||||||
|
frappe.delete_doc(doctype, doc)
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ from frappe.utils.telemetry import capture
|
|||||||
|
|
||||||
|
|
||||||
class CourseChapter(Document):
|
class CourseChapter(Document):
|
||||||
def after_insert(self):
|
pass
|
||||||
capture("chapter_created", "lms")
|
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ class CourseLesson(Document):
|
|||||||
for section in dynamic_documents:
|
for section in dynamic_documents:
|
||||||
self.update_lesson_name_in_document(section)
|
self.update_lesson_name_in_document(section)
|
||||||
|
|
||||||
def after_insert(self):
|
|
||||||
capture("lesson_created", "lms")
|
|
||||||
|
|
||||||
def update_lesson_name_in_document(self, section):
|
def update_lesson_name_in_document(self, section):
|
||||||
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
|
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
|
||||||
macros = find_macros(self.body)
|
macros = find_macros(self.body)
|
||||||
@@ -116,6 +113,8 @@ def save_progress(lesson, course):
|
|||||||
).save(ignore_permissions=True)
|
).save(ignore_permissions=True)
|
||||||
|
|
||||||
progress = get_course_progress(course)
|
progress = get_course_progress(course)
|
||||||
|
capture_progress_for_analytics(progress, course)
|
||||||
|
|
||||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necesary for badge to get assigned.
|
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necesary for badge to get assigned.
|
||||||
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
||||||
enrollment.progress = progress
|
enrollment.progress = progress
|
||||||
@@ -125,6 +124,11 @@ def save_progress(lesson, course):
|
|||||||
return progress
|
return progress
|
||||||
|
|
||||||
|
|
||||||
|
def capture_progress_for_analytics(progress, course):
|
||||||
|
if progress in [25, 50, 75, 100]:
|
||||||
|
capture("course_progress", "lms", {"course": course, "progress": progress})
|
||||||
|
|
||||||
|
|
||||||
def get_quiz_progress(lesson):
|
def get_quiz_progress(lesson):
|
||||||
lesson_details = frappe.db.get_value(
|
lesson_details = frappe.db.get_value(
|
||||||
"Course Lesson", lesson, ["body", "content"], as_dict=1
|
"Course Lesson", lesson, ["body", "content"], as_dict=1
|
||||||
|
|||||||
@@ -11,74 +11,4 @@ from lms.lms.doctype.invite_request.invite_request import (
|
|||||||
|
|
||||||
|
|
||||||
class TestInviteRequest(unittest.TestCase):
|
class TestInviteRequest(unittest.TestCase):
|
||||||
@classmethod
|
pass
|
||||||
def setUpClass(self):
|
|
||||||
create_invite_request("test_invite@example.com")
|
|
||||||
|
|
||||||
def test_create_invite_request(self):
|
|
||||||
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
|
|
||||||
invite = frappe.db.get_value(
|
|
||||||
"Invite Request",
|
|
||||||
filters={"invite_email": "test_invite@example.com"},
|
|
||||||
fieldname=["invite_email", "status", "signup_email"],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
self.assertEqual(invite.status, "Approved")
|
|
||||||
self.assertEqual(invite.signup_email, None)
|
|
||||||
|
|
||||||
def test_create_invite_request_update(self):
|
|
||||||
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"signup_email": "test_invite@example.com",
|
|
||||||
"username": "test_invite",
|
|
||||||
"full_name": "Test Invite",
|
|
||||||
"password": "Test@invite",
|
|
||||||
"invite_code": frappe.db.get_value(
|
|
||||||
"Invite Request", {"invite_email": "test_invite@example.com"}, "name"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
update_invite(data)
|
|
||||||
invite = frappe.db.get_value(
|
|
||||||
"Invite Request",
|
|
||||||
filters={"invite_email": "test_invite@example.com"},
|
|
||||||
fieldname=[
|
|
||||||
"invite_email",
|
|
||||||
"status",
|
|
||||||
"signup_email",
|
|
||||||
"full_name",
|
|
||||||
"username",
|
|
||||||
"invite_code",
|
|
||||||
"name",
|
|
||||||
],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
self.assertEqual(invite.signup_email, "test_invite@example.com")
|
|
||||||
self.assertEqual(invite.full_name, "Test Invite")
|
|
||||||
self.assertEqual(invite.username, "test_invite")
|
|
||||||
self.assertEqual(invite.invite_code, invite.name)
|
|
||||||
self.assertEqual(invite.status, "Registered")
|
|
||||||
|
|
||||||
user = frappe.db.get_value(
|
|
||||||
"User",
|
|
||||||
"test_invite@example.com",
|
|
||||||
fieldname=["first_name", "username", "send_welcome_email", "user_type"],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
self.assertTrue(user)
|
|
||||||
self.assertEqual(user.first_name, invite.full_name.split(" ")[0])
|
|
||||||
self.assertEqual(user.username, invite.username)
|
|
||||||
self.assertEqual(user.send_welcome_email, 0)
|
|
||||||
self.assertEqual(user.user_type, "Website User")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(self):
|
|
||||||
if frappe.db.exists("User", "test_invite@example.com"):
|
|
||||||
frappe.delete_doc("User", "test_invite@example.com")
|
|
||||||
|
|
||||||
invite_request = frappe.db.exists(
|
|
||||||
"Invite Request", {"invite_email": "test_invite@example.com"}
|
|
||||||
)
|
|
||||||
if invite_request:
|
|
||||||
frappe.delete_doc("Invite Request", invite_request)
|
|
||||||
|
|||||||
@@ -8,12 +8,7 @@ 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 (
|
from frappe.utils import cint, format_date, format_datetime, get_time, getdate, add_days
|
||||||
cint,
|
|
||||||
format_date,
|
|
||||||
format_datetime,
|
|
||||||
get_time,
|
|
||||||
)
|
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
get_lessons,
|
get_lessons,
|
||||||
get_lesson_index,
|
get_lesson_index,
|
||||||
@@ -73,21 +68,23 @@ class LMSBatch(Document):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_evaluation_end_date(self):
|
||||||
|
if self.evaluation_end_date and self.evaluation_end_date < self.end_date:
|
||||||
|
frappe.throw(_("Evaluation end date cannot be less than the batch end date."))
|
||||||
|
|
||||||
def send_confirmation_mail(self):
|
def send_confirmation_mail(self):
|
||||||
for student in self.students:
|
for student in self.students:
|
||||||
outgoing_email_account = frappe.get_cached_value(
|
outgoing_email_account = frappe.get_cached_value(
|
||||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||||
)
|
)
|
||||||
if not student.confirmation_email_sent and (
|
if (
|
||||||
outgoing_email_account or frappe.conf.get("mail_login")
|
not student.confirmation_email_sent
|
||||||
|
and getdate(student.creation) >= add_days(getdate(), -2)
|
||||||
|
and (outgoing_email_account or frappe.conf.get("mail_login"))
|
||||||
):
|
):
|
||||||
self.send_mail(student)
|
self.send_mail(student)
|
||||||
student.confirmation_email_sent = 1
|
student.confirmation_email_sent = 1
|
||||||
|
|
||||||
def validate_evaluation_end_date(self):
|
|
||||||
if self.evaluation_end_date and self.evaluation_end_date < self.end_date:
|
|
||||||
frappe.throw(_("Evaluation end date cannot be less than the batch end date."))
|
|
||||||
|
|
||||||
def send_mail(self, student):
|
def send_mail(self, student):
|
||||||
subject = _("Enrollment Confirmation for the Next Training Batch")
|
subject = _("Enrollment Confirmation for the Next Training Batch")
|
||||||
template = "batch_confirmation"
|
template = "batch_confirmation"
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
"template",
|
"template",
|
||||||
"published",
|
"published",
|
||||||
"section_break_scyf",
|
"section_break_scyf",
|
||||||
"expiry_date",
|
"evaluator",
|
||||||
|
"evaluator_name",
|
||||||
"column_break_slaw",
|
"column_break_slaw",
|
||||||
|
"expiry_date",
|
||||||
"batch_name"
|
"batch_name"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -95,11 +97,24 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_slaw",
|
"fieldname": "column_break_slaw",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "evaluator",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Evaluator",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "evaluator.full_name",
|
||||||
|
"fieldname": "evaluator_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Evaluator Name",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-16 15:29:19.708888",
|
"modified": "2024-09-11 11:37:20.419955",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
|
|||||||
@@ -8,12 +8,16 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"column_break_ueht",
|
||||||
"course",
|
"course",
|
||||||
|
"batch_name",
|
||||||
|
"section_break_zwfi",
|
||||||
|
"evaluator",
|
||||||
|
"evaluator_name",
|
||||||
"column_break_5",
|
"column_break_5",
|
||||||
"date",
|
"date",
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"batch_name",
|
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"rating",
|
"rating",
|
||||||
"status",
|
"status",
|
||||||
@@ -103,11 +107,33 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Batch Name",
|
"label": "Batch Name",
|
||||||
"options": "LMS Batch"
|
"options": "LMS Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ueht",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_zwfi",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "evaluator",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Evaluator",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "evaluator.full_name",
|
||||||
|
"fieldname": "evaluator_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Evaluator Name",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-16 14:06:11.977666",
|
"modified": "2024-09-11 11:20:06.233491",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Evaluation",
|
"name": "LMS Certificate Evaluation",
|
||||||
|
|||||||
@@ -7,11 +7,17 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"course",
|
"course",
|
||||||
"evaluator",
|
"course_title",
|
||||||
"batch_name",
|
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"section_break_ikne",
|
||||||
|
"evaluator",
|
||||||
|
"evaluator_name",
|
||||||
|
"column_break_sjco",
|
||||||
|
"batch_name",
|
||||||
|
"batch_title",
|
||||||
|
"timezone",
|
||||||
"section_break_lifi",
|
"section_break_lifi",
|
||||||
"date",
|
"date",
|
||||||
"day",
|
"day",
|
||||||
@@ -33,7 +39,6 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "member",
|
"fieldname": "member",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Member",
|
"label": "Member",
|
||||||
"options": "User",
|
"options": "User",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -41,9 +46,9 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "evaluator",
|
"fieldname": "evaluator",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Evaluator",
|
"label": "Evaluator",
|
||||||
"options": "User",
|
"options": "User"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "date",
|
"fieldname": "date",
|
||||||
@@ -103,11 +108,47 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Batch",
|
"label": "Batch",
|
||||||
"options": "LMS Batch"
|
"options": "LMS Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ikne",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_sjco",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "course.title",
|
||||||
|
"fieldname": "course_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Course Title",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "evaluator.full_name",
|
||||||
|
"fieldname": "evaluator_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Evaluator Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "batch_name.timezone",
|
||||||
|
"fieldname": "timezone",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Timezone",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "batch_name.title",
|
||||||
|
"fieldname": "batch_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Batch Title"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-16 11:01:28.336807",
|
"modified": "2024-09-11 11:19:44.669132",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
|
|||||||
@@ -34,23 +34,27 @@ class LMSCertificateRequest(Document):
|
|||||||
self.evaluator = get_evaluator(self.course, self.batch_name)
|
self.evaluator = get_evaluator(self.course, self.batch_name)
|
||||||
|
|
||||||
def validate_unavailability(self):
|
def validate_unavailability(self):
|
||||||
unavailable = frappe.db.get_value(
|
if self.evaluator:
|
||||||
"Course Evaluator", self.evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
|
unavailable = frappe.db.get_value(
|
||||||
)
|
"Course Evaluator",
|
||||||
if (
|
self.evaluator,
|
||||||
unavailable.unavailable_from
|
["unavailable_from", "unavailable_to"],
|
||||||
and unavailable.unavailable_to
|
as_dict=1,
|
||||||
and getdate(self.date) >= unavailable.unavailable_from
|
|
||||||
and getdate(self.date) <= unavailable.unavailable_to
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
|
|
||||||
).format(
|
|
||||||
format_date(unavailable.unavailable_from, "medium"),
|
|
||||||
format_date(unavailable.unavailable_to, "medium"),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
unavailable.unavailable_from
|
||||||
|
and unavailable.unavailable_to
|
||||||
|
and getdate(self.date) >= unavailable.unavailable_from
|
||||||
|
and getdate(self.date) <= unavailable.unavailable_to
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
|
||||||
|
).format(
|
||||||
|
format_date(unavailable.unavailable_from, "medium"),
|
||||||
|
format_date(unavailable.unavailable_to, "medium"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_slot(self):
|
def validate_slot(self):
|
||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
@@ -120,14 +124,12 @@ class LMSCertificateRequest(Document):
|
|||||||
template = "certificate_request_notification"
|
template = "certificate_request_notification"
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"course": frappe.db.get_value("LMS Course", self.course, "title"),
|
"course": self.course_title,
|
||||||
"timezone": frappe.db.get_value("LMS Batch", self.batch_name, "timezone")
|
"timezone": self.timezone if self.batch_name else "",
|
||||||
if self.batch_name
|
|
||||||
else "",
|
|
||||||
"date": format_date(self.date, "medium"),
|
"date": format_date(self.date, "medium"),
|
||||||
"member_name": self.member_name,
|
"member_name": self.member_name,
|
||||||
"start_time": format_time(self.start_time, "short"),
|
"start_time": format_time(self.start_time, "short"),
|
||||||
"evaluator": frappe.db.get_value("User", self.evaluator, "full_name"),
|
"evaluator": self.evaluator_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ class LMSCourse(Document):
|
|||||||
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()
|
||||||
|
|
||||||
def after_insert(self):
|
|
||||||
capture("course_created", "lms")
|
|
||||||
|
|
||||||
def send_email_to_interested_users(self):
|
def send_email_to_interested_users(self):
|
||||||
interested_users = frappe.get_all(
|
interested_users = frappe.get_all(
|
||||||
"LMS Course Interest", {"course": self.name}, ["name", "user"]
|
"LMS Course Interest", {"course": self.name}, ["name", "user"]
|
||||||
|
|||||||
@@ -90,21 +90,19 @@ def quiz_summary(quiz, results):
|
|||||||
|
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value(
|
||||||
"LMS Quiz Question",
|
"LMS Quiz Question",
|
||||||
{"parent": quiz, "idx": result["question_index"]},
|
{"parent": quiz, "question": result["question_name"]},
|
||||||
["question", "marks"],
|
["question", "marks", "question_detail"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
result["question_name"] = question_details.question
|
result["question_name"] = question_details.question
|
||||||
result["question"] = frappe.db.get_value(
|
result["question"] = question_details.question_detail
|
||||||
"LMS Question", question_details.question, "question"
|
|
||||||
)
|
|
||||||
marks = question_details.marks if correct else 0
|
marks = question_details.marks if correct else 0
|
||||||
|
|
||||||
result["marks"] = marks
|
result["marks"] = marks
|
||||||
score += marks
|
score += marks
|
||||||
|
|
||||||
del result["question_index"]
|
del result["question_name"]
|
||||||
|
|
||||||
quiz_details = frappe.db.get_value(
|
quiz_details = frappe.db.get_value(
|
||||||
"LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1
|
"LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1
|
||||||
@@ -297,15 +295,6 @@ def check_choice_answers(question, answers):
|
|||||||
|
|
||||||
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
|
|
||||||
""" if question_details.multiple:
|
|
||||||
correct_answers = [ question_details[f"option_{num}"] for num in range(1,5) if question_details[f"is_correct_{num}"]]
|
|
||||||
print(answers)
|
|
||||||
for ans in correct_answers:
|
|
||||||
if ans not in answers:
|
|
||||||
is_correct.append(0)
|
|
||||||
else:
|
|
||||||
is_correct.append(1)
|
|
||||||
else: """
|
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
if question_details[f"option_{num}"] in answers:
|
if question_details[f"option_{num}"] in answers:
|
||||||
is_correct.append(question_details[f"is_correct_{num}"])
|
is_correct.append(question_details[f"is_correct_{num}"])
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
|
|
||||||
{% set timezone = frappe.db.get_value("LMS Batch", doc.batch, "timezone") %}
|
|
||||||
{% set timezone = timezone if timezone else '' %}
|
|
||||||
{% set evaluator_name = frappe.db.get_value("User", doc.evaluator, "full_name") %}
|
|
||||||
|
|
||||||
<p> {{ _("Hey {0}").format(doc.member_name) }} </p>
|
|
||||||
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short"), timezone) }}</p>
|
|
||||||
<p> {{ _("Your evaluator is {0}").format(evaluator_name) }} </p>
|
|
||||||
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"attach_print": 0,
|
|
||||||
"channel": "Email",
|
|
||||||
"creation": "2022-06-03 11:02:34.579145",
|
|
||||||
"days_in_advance": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Notification",
|
|
||||||
"document_type": "LMS Certificate Request",
|
|
||||||
"enabled": 0,
|
|
||||||
"event": "New",
|
|
||||||
"idx": 0,
|
|
||||||
"is_standard": 1,
|
|
||||||
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n{% set timezone = frappe.db.get_value(\"LMS Batch\", doc.batch, \"timezone\") %}\n{% set timezone = timezone if timezone else '' %}\n{% set evaluator_name = frappe.db.get_value(\"User\", doc.evaluator, \"full_name\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), timezone) }}</p>\n<p> {{ _(\"Your evaluator is {0}\").format(evaluator_name) }} </p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
|
||||||
"message_type": "HTML",
|
|
||||||
"modified": "2024-08-01 12:17:40.647724",
|
|
||||||
"modified_by": "jannat@frappe.io",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "Certificate Request Creation",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"recipients": [
|
|
||||||
{
|
|
||||||
"receiver_by_document_field": "member"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"receiver_by_document_field": "evaluator"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"receiver_by_role": "Frappe School Admin"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"send_system_notification": 0,
|
|
||||||
"send_to_all_assignees": 0,
|
|
||||||
"subject": "Your evaluation slot has been booked"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
# do your magic here
|
|
||||||
pass
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
|
<p> {{ _("Hey {0}").format(doc.member_name) }} </p>
|
||||||
{% set timezone = frappe.db.get_value("LMS Batch", doc.batch, "timezone") %}
|
<br>
|
||||||
{% set timezone = timezone if timezone else '' %}
|
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(doc.course_title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short"), doc.timezone) }}</p>
|
||||||
{% set evaluator_name = frappe.db.get_value("User", doc.evaluator, "full_name") %}
|
<br>
|
||||||
|
<p> {{ _("{0} is your evaluator").format(doc.evaluator_name) }} </p>
|
||||||
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short"), timezone) }}</p>
|
<br>
|
||||||
<p> {{ _("Your evaluator is {0}").format(evaluator_name) }}
|
|
||||||
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
"event": "Days Before",
|
"event": "Days Before",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n{% set timezone = frappe.db.get_value(\"LMS Batch\", doc.batch, \"timezone\") %}\n{% set timezone = timezone if timezone else '' %}\n{% set evaluator_name = frappe.db.get_value(\"User\", doc.evaluator, \"full_name\") %}\n\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), timezone) }}</p>\n<p> {{ _(\"Your evaluator is {0}\").format(evaluator_name) }}\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
"message": "<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<br>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(doc.course_title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), doc.timezone) }}</p>\n<br>\n<p> {{ _(\"{0} is your evaluator\").format(doc.evaluator_name) }} </p>\n<br>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
||||||
"message_type": "HTML",
|
"message_type": "HTML",
|
||||||
"modified": "2024-07-10 15:51:33.803704",
|
"modified": "2024-09-05 16:33:42.212842",
|
||||||
"modified_by": "sayali@erpnext.com",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Certificate Request Reminder",
|
"name": "Certificate Request Reminder",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Frappe LMS VERSION\n"
|
"Project-Id-Version: Frappe LMS VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: school@frappe.io\n"
|
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||||
"POT-Creation-Date: 2024-08-09 16:04+0000\n"
|
"POT-Creation-Date: 2024-09-13 16:04+0000\n"
|
||||||
"PO-Revision-Date: 2024-08-09 16:04+0000\n"
|
"PO-Revision-Date: 2024-09-13 16:04+0000\n"
|
||||||
"Last-Translator: school@frappe.io\n"
|
"Last-Translator: jannat@frappe.io\n"
|
||||||
"Language-Team: school@frappe.io\n"
|
"Language-Team: jannat@frappe.io\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
@@ -20,6 +20,46 @@ msgstr ""
|
|||||||
msgid " Please evaluate and grade it."
|
msgid " Please evaluate and grade it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/app/lms-settings/LMS%20Settings\">LMS Settings</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/app/web-page/new-web-page-1\">Setup a Home Page</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/lms/courses\">Visit LMS Portal</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/lms/courses/new/edit\">Create a Course</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"https://docs.frappe.io/learning\">Documentation</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Header text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<span class=\"h4\"><b>Get Started</b></span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Header text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<span class=\"h4\"><b>Master</b></span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Header text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the verify_terms (Check) field in DocType 'User'
|
#. Label of the verify_terms (Check) field in DocType 'User'
|
||||||
#: fixtures/custom_field.json
|
#: fixtures/custom_field.json
|
||||||
msgid "Acceptance for Terms and/or Policies"
|
msgid "Acceptance for Terms and/or Policies"
|
||||||
@@ -48,7 +88,7 @@ msgstr ""
|
|||||||
msgid "Add a Lesson"
|
msgid "Add a Lesson"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_question/lms_question.py:59
|
#: lms/doctype/lms_question/lms_question.py:60
|
||||||
msgid "Add at least one possible answer for this question: {0}"
|
msgid "Add at least one possible answer for this question: {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -98,12 +138,7 @@ msgstr ""
|
|||||||
msgid "Allow accessing future dates"
|
msgid "Allow accessing future dates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the allow_student_progress (Check) field in DocType 'LMS Settings'
|
#: overrides/user.py:199
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Allow students to see each others progress in class"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: overrides/user.py:195
|
|
||||||
msgid "Already Registered"
|
msgid "Already Registered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -136,12 +171,6 @@ msgstr ""
|
|||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Option for the 'Course Creation Access Through Website To' (Select) field in
|
|
||||||
#. DocType 'LMS Settings'
|
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Anyone"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Label of the apply_gst (Check) field in DocType 'LMS Settings'
|
#. Label of the apply_gst (Check) field in DocType 'LMS Settings'
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
#: lms/doctype/lms_settings/lms_settings.json
|
||||||
msgid "Apply GST for India"
|
msgid "Apply GST for India"
|
||||||
@@ -233,7 +262,7 @@ msgstr ""
|
|||||||
msgid "Assignment will appear at the bottom of the lesson."
|
msgid "Assignment will appear at the bottom of the lesson."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_question/lms_question.py:41
|
#: lms/doctype/lms_question/lms_question.py:42
|
||||||
msgid "At least one option must be correct for this question."
|
msgid "At least one option must be correct for this question."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -349,6 +378,11 @@ msgstr ""
|
|||||||
msgid "Batch Student"
|
msgid "Batch Student"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. Label of the batch_title (Data) field in DocType 'LMS Certificate Request'
|
||||||
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||||
|
msgid "Batch Title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: public/js/common_functions.js:427
|
#: public/js/common_functions.js:427
|
||||||
msgid "Batch Updated"
|
msgid "Batch Updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -364,7 +398,7 @@ msgstr ""
|
|||||||
#. Group in LMS Course's connections
|
#. Group in LMS Course's connections
|
||||||
#. Label of the batches (Check) field in DocType 'LMS Settings'
|
#. Label of the batches (Check) field in DocType 'LMS Settings'
|
||||||
#: lms/doctype/lms_course/lms_course.json
|
#: lms/doctype/lms_course/lms_course.json
|
||||||
#: lms/doctype/lms_settings/lms_settings.json www/lms.py:61
|
#: lms/doctype/lms_settings/lms_settings.json www/lms.py:60
|
||||||
msgid "Batches"
|
msgid "Batches"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -850,11 +884,6 @@ msgstr ""
|
|||||||
msgid "Course Content"
|
msgid "Course Content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the portal_course_creation (Select) field in DocType 'LMS Settings'
|
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Course Creation Access Through Website To"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Name of a role
|
#. Name of a role
|
||||||
#: lms/doctype/lms_course/lms_course.json
|
#: lms/doctype/lms_course/lms_course.json
|
||||||
#: lms/doctype/lms_question/lms_question.json
|
#: lms/doctype/lms_question/lms_question.json
|
||||||
@@ -862,12 +891,6 @@ msgstr ""
|
|||||||
msgid "Course Creator"
|
msgid "Course Creator"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Option for the 'Course Creation Access Through Website To' (Select) field in
|
|
||||||
#. DocType 'LMS Settings'
|
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Course Creator Role"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Label of a Card Break in the LMS Workspace
|
#. Label of a Card Break in the LMS Workspace
|
||||||
#: lms/workspace/lms/lms.json
|
#: lms/workspace/lms/lms.json
|
||||||
msgid "Course Data"
|
msgid "Course Data"
|
||||||
@@ -888,15 +911,10 @@ msgstr ""
|
|||||||
msgid "Course Lesson"
|
msgid "Course Lesson"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: www/lms.py:28
|
#: www/lms.py:27
|
||||||
msgid "Course List"
|
msgid "Course List"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the search_placeholder (Data) field in DocType 'LMS Settings'
|
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Course List Search Bar Placeholder"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lms/report/course_progress_summary/course_progress_summary.py:58
|
#: lms/report/course_progress_summary/course_progress_summary.py:58
|
||||||
msgid "Course Name"
|
msgid "Course Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -912,10 +930,7 @@ msgid "Course Progress Summary"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the section_break_7 (Section Break) field in DocType 'LMS Course'
|
#. Label of the section_break_7 (Section Break) field in DocType 'LMS Course'
|
||||||
#. Label of the course_settings_section (Section Break) field in DocType 'LMS
|
|
||||||
#. Settings'
|
|
||||||
#: lms/doctype/lms_course/lms_course.json
|
#: lms/doctype/lms_course/lms_course.json
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Course Settings"
|
msgid "Course Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -926,8 +941,10 @@ msgstr ""
|
|||||||
|
|
||||||
#. Label of the title (Data) field in DocType 'Batch Course'
|
#. Label of the title (Data) field in DocType 'Batch Course'
|
||||||
#. Label of the course_title (Data) field in DocType 'LMS Certificate'
|
#. Label of the course_title (Data) field in DocType 'LMS Certificate'
|
||||||
|
#. Label of the course_title (Data) field in DocType 'LMS Certificate Request'
|
||||||
#: lms/doctype/batch_course/batch_course.json
|
#: lms/doctype/batch_course/batch_course.json
|
||||||
#: lms/doctype/lms_certificate/lms_certificate.json
|
#: lms/doctype/lms_certificate/lms_certificate.json
|
||||||
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||||
msgid "Course Title"
|
msgid "Course Title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1116,7 +1133,7 @@ msgstr ""
|
|||||||
msgid "Dream Companies"
|
msgid "Dream Companies"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_question/lms_question.py:31
|
#: lms/doctype/lms_question/lms_question.py:32
|
||||||
msgid "Duplicate options found for this question."
|
msgid "Duplicate options found for this question."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1254,7 +1271,7 @@ msgstr ""
|
|||||||
msgid "Enter the correct answer"
|
msgid "Enter the correct answer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1088
|
#: lms/utils.py:1081
|
||||||
msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
|
msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1287,15 +1304,30 @@ msgstr ""
|
|||||||
#. Label of the evaluator (Link) field in DocType 'Batch Course'
|
#. Label of the evaluator (Link) field in DocType 'Batch Course'
|
||||||
#. Label of the evaluator (Link) field in DocType 'Course Evaluator'
|
#. Label of the evaluator (Link) field in DocType 'Course Evaluator'
|
||||||
#. Label of the evaluator (Link) field in DocType 'LMS Assignment Submission'
|
#. Label of the evaluator (Link) field in DocType 'LMS Assignment Submission'
|
||||||
|
#. Label of the evaluator (Link) field in DocType 'LMS Certificate'
|
||||||
|
#. Label of the evaluator (Link) field in DocType 'LMS Certificate Evaluation'
|
||||||
#. Label of the evaluator (Link) field in DocType 'LMS Certificate Request'
|
#. Label of the evaluator (Link) field in DocType 'LMS Certificate Request'
|
||||||
#: lms/doctype/batch_course/batch_course.json
|
#: lms/doctype/batch_course/batch_course.json
|
||||||
#: lms/doctype/course_evaluator/course_evaluator.json
|
#: lms/doctype/course_evaluator/course_evaluator.json
|
||||||
#: lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
#: lms/doctype/lms_assignment_submission/lms_assignment_submission.json
|
||||||
|
#: lms/doctype/lms_certificate/lms_certificate.json
|
||||||
|
#: lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json
|
||||||
#: lms/doctype/lms_certificate_request/lms_certificate_request.json
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||||
#: templates/upcoming_evals.html:33
|
#: templates/upcoming_evals.html:33
|
||||||
msgid "Evaluator"
|
msgid "Evaluator"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. Label of the evaluator_name (Data) field in DocType 'LMS Certificate'
|
||||||
|
#. Label of the evaluator_name (Data) field in DocType 'LMS Certificate
|
||||||
|
#. Evaluation'
|
||||||
|
#. Label of the evaluator_name (Data) field in DocType 'LMS Certificate
|
||||||
|
#. Request'
|
||||||
|
#: lms/doctype/lms_certificate/lms_certificate.json
|
||||||
|
#: lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json
|
||||||
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||||
|
msgid "Evaluator Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Name of a DocType
|
#. Name of a DocType
|
||||||
#: lms/doctype/evaluator_schedule/evaluator_schedule.json
|
#: lms/doctype/evaluator_schedule/evaluator_schedule.json
|
||||||
msgid "Evaluator Schedule"
|
msgid "Evaluator Schedule"
|
||||||
@@ -1538,7 +1570,8 @@ msgstr ""
|
|||||||
msgid "Here are a few courses we recommend for you to get started with {0}"
|
msgid "Here are a few courses we recommend for you to get started with {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:6
|
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:1
|
||||||
|
#: templates/emails/certificate_request_notification.html:1
|
||||||
msgid "Hey {0}"
|
msgid "Hey {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1738,7 +1771,7 @@ msgstr ""
|
|||||||
msgid "Invalid Start or End Time."
|
msgid "Invalid Start or End Time."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:932
|
#: lms/utils.py:925
|
||||||
msgid "Invalid document provided."
|
msgid "Invalid document provided."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1822,7 +1855,7 @@ msgstr ""
|
|||||||
msgid "Job Board Title"
|
msgid "Job Board Title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: www/lms.py:109
|
#: www/lms.py:108
|
||||||
msgid "Job Openings"
|
msgid "Job Openings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2328,7 +2361,6 @@ msgstr ""
|
|||||||
|
|
||||||
#. Label of the mentor_request_section (Section Break) field in DocType 'LMS
|
#. Label of the mentor_request_section (Section Break) field in DocType 'LMS
|
||||||
#. Settings'
|
#. Settings'
|
||||||
#. Label of the mentor_request_tab (Tab Break) field in DocType 'LMS Settings'
|
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
#: lms/doctype/lms_settings/lms_settings.json
|
||||||
msgid "Mentor Request"
|
msgid "Mentor Request"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2385,11 +2417,11 @@ msgstr ""
|
|||||||
msgid "Modified By"
|
msgid "Modified By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:188
|
#: lms/api.py:190
|
||||||
msgid "Module Name is incorrect or does not exist."
|
msgid "Module Name is incorrect or does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:184
|
#: lms/api.py:186
|
||||||
msgid "Module is incorrect."
|
msgid "Module is incorrect."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2414,11 +2446,11 @@ msgstr ""
|
|||||||
msgid "New Assignment Submission"
|
msgid "New Assignment Submission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: public/js/common_functions.js:255 www/lms.py:87
|
#: public/js/common_functions.js:255 www/lms.py:86
|
||||||
msgid "New Batch"
|
msgid "New Batch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: www/lms.py:38
|
#: www/lms.py:37
|
||||||
msgid "New Course"
|
msgid "New Course"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2430,11 +2462,11 @@ msgstr ""
|
|||||||
msgid "New Sign Up"
|
msgid "New Sign Up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:619
|
#: lms/utils.py:612
|
||||||
msgid "New comment in batch {0}"
|
msgid "New comment in batch {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:612
|
#: lms/utils.py:605
|
||||||
msgid "New reply on the topic {0} in course {1}"
|
msgid "New reply on the topic {0} in course {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2471,11 +2503,6 @@ msgstr ""
|
|||||||
msgid "No courses under review"
|
msgid "No courses under review"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/search_course/search_course.html:61
|
|
||||||
#: templates/search_course/search_course.js:47
|
|
||||||
msgid "No result found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/course_list.html:13
|
#: templates/course_list.html:13
|
||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2484,7 +2511,7 @@ msgstr ""
|
|||||||
msgid "No."
|
msgid "No."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:190
|
#: overrides/user.py:194
|
||||||
msgid "Not Allowed"
|
msgid "Not Allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2719,7 +2746,7 @@ msgstr ""
|
|||||||
msgid "Payment for Document Type"
|
msgid "Payment for Document Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:949
|
#: lms/utils.py:942
|
||||||
msgid "Payment for {0} course"
|
msgid "Payment for {0} course"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2749,11 +2776,11 @@ msgstr ""
|
|||||||
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
|
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:236
|
#: overrides/user.py:240
|
||||||
msgid "Please ask your administrator to verify your sign-up"
|
msgid "Please ask your administrator to verify your sign-up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:234
|
#: overrides/user.py:238
|
||||||
msgid "Please check your email for verification"
|
msgid "Please check your email for verification"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2778,12 +2805,12 @@ msgstr ""
|
|||||||
msgid "Please enter your answer"
|
msgid "Please enter your answer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:180
|
#: lms/api.py:182
|
||||||
msgid "Please login to continue with payment."
|
msgid "Please login to continue with payment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:9
|
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
|
||||||
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:8
|
#: templates/emails/certificate_request_notification.html:7
|
||||||
msgid "Please prepare well and be on time for the evaluations."
|
msgid "Please prepare well and be on time for the evaluations."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2952,6 +2979,11 @@ msgstr ""
|
|||||||
msgid "Question "
|
msgid "Question "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. Label of the question_detail (Text) field in DocType 'LMS Quiz Question'
|
||||||
|
#: lms/doctype/lms_quiz_question/lms_quiz_question.json
|
||||||
|
msgid "Question Detail"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the question_name (Link) field in DocType 'LMS Quiz Result'
|
#. Label of the question_name (Link) field in DocType 'LMS Quiz Result'
|
||||||
#: lms/doctype/lms_quiz_result/lms_quiz_result.json
|
#: lms/doctype/lms_quiz_result/lms_quiz_result.json
|
||||||
msgid "Question Name"
|
msgid "Question Name"
|
||||||
@@ -3048,7 +3080,7 @@ msgstr ""
|
|||||||
msgid "Registered"
|
msgid "Registered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:197
|
#: overrides/user.py:201
|
||||||
msgid "Registered but disabled"
|
msgid "Registered but disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3214,11 +3246,6 @@ msgstr ""
|
|||||||
msgid "Set your Password"
|
msgid "Set your Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the section_break_hsiv (Section Break) field in DocType 'LMS Quiz'
|
|
||||||
#: lms/doctype/lms_quiz/lms_quiz.json
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
|
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
|
||||||
#: lms/doctype/lms_course/lms_course.json
|
#: lms/doctype/lms_course/lms_course.json
|
||||||
msgid "Short Introduction"
|
msgid "Short Introduction"
|
||||||
@@ -3289,7 +3316,7 @@ msgstr ""
|
|||||||
msgid "Sidebar Items"
|
msgid "Sidebar Items"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:190
|
#: overrides/user.py:194
|
||||||
msgid "Sign Up is disabled"
|
msgid "Sign Up is disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3333,7 +3360,7 @@ msgstr ""
|
|||||||
msgid "Skills"
|
msgid "Skills"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:38
|
#: overrides/user.py:42
|
||||||
msgid "Skills must be unique"
|
msgid "Skills must be unique"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3441,7 +3468,7 @@ msgid "Startup Organization"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the statistics (Check) field in DocType 'LMS Settings'
|
#. Label of the statistics (Check) field in DocType 'LMS Settings'
|
||||||
#: lms/doctype/lms_settings/lms_settings.json www/lms.py:134
|
#: lms/doctype/lms_settings/lms_settings.json www/lms.py:133
|
||||||
msgid "Statistics"
|
msgid "Statistics"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3625,7 +3652,7 @@ msgstr ""
|
|||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:201
|
#: overrides/user.py:205
|
||||||
msgid "Temporarily Disabled"
|
msgid "Temporarily Disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3670,7 +3697,7 @@ msgstr ""
|
|||||||
msgid "The course {0} is now available on {1}."
|
msgid "The course {0} is now available on {1}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:44
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:51
|
||||||
msgid "The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
|
msgid "The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3678,7 +3705,7 @@ msgstr ""
|
|||||||
msgid "The quiz has a time limit. For each question you will be given {0} seconds."
|
msgid "The quiz has a time limit. For each question you will be given {0} seconds."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:62
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:69
|
||||||
msgid "The slot is already booked by another participant."
|
msgid "The slot is already booked by another participant."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3694,7 +3721,7 @@ msgstr ""
|
|||||||
msgid "There are no {0} on this site."
|
msgid "There are no {0} on this site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1070
|
#: lms/utils.py:1063
|
||||||
msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3709,7 +3736,7 @@ msgstr ""
|
|||||||
msgid "This certificate does no expire"
|
msgid "This certificate does no expire"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1028 lms/utils.py:1769
|
#: lms/utils.py:1021 lms/utils.py:1762
|
||||||
msgid "This course is free."
|
msgid "This course is free."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3764,8 +3791,10 @@ msgid "Timetable Template"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the timezone (Data) field in DocType 'LMS Batch'
|
#. Label of the timezone (Data) field in DocType 'LMS Batch'
|
||||||
|
#. Label of the timezone (Data) field in DocType 'LMS Certificate Request'
|
||||||
#. Label of the timezone (Data) field in DocType 'LMS Live Class'
|
#. Label of the timezone (Data) field in DocType 'LMS Live Class'
|
||||||
#: lms/doctype/lms_batch/lms_batch.json
|
#: lms/doctype/lms_batch/lms_batch.json
|
||||||
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||||
#: lms/doctype/lms_live_class/lms_live_class.json
|
#: lms/doctype/lms_live_class/lms_live_class.json
|
||||||
msgid "Timezone"
|
msgid "Timezone"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3823,11 +3852,11 @@ msgstr ""
|
|||||||
msgid "To Date is mandatory in Work Experience."
|
msgid "To Date is mandatory in Work Experience."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1037 lms/utils.py:1780
|
#: lms/utils.py:1030 lms/utils.py:1773
|
||||||
msgid "To join this batch, please contact the Administrator."
|
msgid "To join this batch, please contact the Administrator."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:202
|
#: overrides/user.py:206
|
||||||
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4077,15 +4106,15 @@ msgstr ""
|
|||||||
msgid "Write a review"
|
msgid "Write a review"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:86
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:93
|
||||||
msgid "You already have an evaluation on {0} at {1} for the course {2}."
|
msgid "You already have an evaluation on {0} at {1} for the course {2}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:204
|
#: lms/api.py:206
|
||||||
msgid "You are already enrolled for this batch."
|
msgid "You are already enrolled for this batch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:196
|
#: lms/api.py:198
|
||||||
msgid "You are already enrolled for this course."
|
msgid "You are already enrolled for this course."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4110,11 +4139,11 @@ msgstr ""
|
|||||||
msgid "You can find their resume attached to this email."
|
msgid "You can find their resume attached to this email."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:106
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:113
|
||||||
msgid "You cannot schedule evaluations after {0}."
|
msgid "You cannot schedule evaluations after {0}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:95
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:102
|
||||||
msgid "You cannot schedule evaluations for past slots."
|
msgid "You cannot schedule evaluations for past slots."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4122,7 +4151,7 @@ msgstr ""
|
|||||||
msgid "You don't have any notifications."
|
msgid "You don't have any notifications."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:137
|
||||||
msgid "You got"
|
msgid "You got"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4174,13 +4203,16 @@ msgstr ""
|
|||||||
msgid "Your Account has been successfully created!"
|
msgid "Your Account has been successfully created!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:7
|
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:3
|
||||||
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:6
|
#: templates/emails/certificate_request_notification.html:3
|
||||||
msgid "Your evaluation for the course {0} has been scheduled on {1} at {2} {3}."
|
msgid "Your evaluation for the course {0} has been scheduled on {1} at {2} {3}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:8
|
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:123
|
||||||
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
|
msgid "Your evaluation slot has been booked"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/emails/certificate_request_notification.html:5
|
||||||
msgid "Your evaluator is {0}"
|
msgid "Your evaluator is {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4188,7 +4220,7 @@ msgstr ""
|
|||||||
msgid "Your request to join us as a mentor for the course"
|
msgid "Your request to join us as a mentor for the course"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:140
|
||||||
msgid "Your score is"
|
msgid "Your score is"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4201,7 +4233,7 @@ msgstr ""
|
|||||||
msgid "cancel your application"
|
msgid "cancel your application"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:137
|
||||||
msgid "correct answers"
|
msgid "correct answers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4217,7 +4249,7 @@ msgstr ""
|
|||||||
msgid "of"
|
msgid "of"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:141
|
||||||
msgid "out of"
|
msgid "out of"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4261,7 +4293,11 @@ msgstr ""
|
|||||||
msgid "{0} is already certified for the course {1}"
|
msgid "{0} is already certified for the course {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:696
|
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:5
|
||||||
|
msgid "{0} is your evaluator"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lms/utils.py:689
|
||||||
msgid "{0} mentioned you in a comment"
|
msgid "{0} mentioned you in a comment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4269,7 +4305,7 @@ msgstr ""
|
|||||||
msgid "{0} mentioned you in a comment in your batch."
|
msgid "{0} mentioned you in a comment in your batch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:649 lms/utils.py:655
|
#: lms/utils.py:642 lms/utils.py:648
|
||||||
msgid "{0} mentioned you in a comment in {1}"
|
msgid "{0} mentioned you in a comment in {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class CustomUser(User):
|
|||||||
super().validate()
|
super().validate()
|
||||||
self.validate_username_duplicates()
|
self.validate_username_duplicates()
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
super().after_insert()
|
||||||
|
self.add_roles("LMS Student")
|
||||||
|
|
||||||
def validate_username_duplicates(self):
|
def validate_username_duplicates(self):
|
||||||
while not self.username or self.username_exists():
|
while not self.username or self.username_exists():
|
||||||
self.username = append_number_if_name_exists(
|
self.username = append_number_if_name_exists(
|
||||||
|
|||||||
@@ -89,4 +89,5 @@ lms.patches.v1_0.change_navbar_urls
|
|||||||
lms.patches.v1_0.set_published_on
|
lms.patches.v1_0.set_published_on
|
||||||
lms.patches.v2_0.fix_progress_percentage
|
lms.patches.v2_0.fix_progress_percentage
|
||||||
lms.patches.v2_0.add_discussion_topic_titles
|
lms.patches.v2_0.add_discussion_topic_titles
|
||||||
lms.patches.v2_0.sidebar_settings
|
lms.patches.v2_0.sidebar_settings
|
||||||
|
lms.patches.v2_0.delete_certificate_request_notification
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.delete("Notification", "Certificate Request Creation")
|
||||||
@@ -2202,7 +2202,7 @@ select {
|
|||||||
.rows .grid-row .data-row,
|
.rows .grid-row .data-row,
|
||||||
.rows .grid-row .grid-footer-toolbar,
|
.rows .grid-row .grid-footer-toolbar,
|
||||||
.grid-form-heading {
|
.grid-form-heading {
|
||||||
cursor: none;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-header {
|
.schedule-header {
|
||||||
|
|||||||
BIN
lms/public/images/desk.png
Normal file
BIN
lms/public/images/desk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -1,4 +1,7 @@
|
|||||||
<p> {{ _("Hey {0}").format(member_name) }} </p>
|
<p> {{ _("Hey {0}").format(member_name) }} </p>
|
||||||
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, date, start_time, timezone) }}</p>
|
<br>
|
||||||
|
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(course, date, start_time, timezone) }}</p>
|
||||||
|
<br>
|
||||||
<p> {{ _("Your evaluator is {0}").format(evaluator) }} </p>
|
<p> {{ _("Your evaluator is {0}").format(evaluator) }} </p>
|
||||||
|
<br>
|
||||||
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ def get_context():
|
|||||||
csrf_token = frappe.sessions.get_csrf_token()
|
csrf_token = frappe.sessions.get_csrf_token()
|
||||||
frappe.db.commit() # nosemgrep
|
frappe.db.commit() # nosemgrep
|
||||||
context.csrf_token = csrf_token
|
context.csrf_token = csrf_token
|
||||||
if frappe.session.user != "Guest":
|
capture("active_site", "lms")
|
||||||
capture("active_site", "lms")
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -149,8 +148,9 @@ def get_meta(app_path):
|
|||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
soup = BeautifulSoup(user.bio, "html.parser")
|
if user.bio:
|
||||||
user.bio = soup.get_text()
|
soup = BeautifulSoup(user.bio, "html.parser")
|
||||||
|
user.bio = soup.get_text()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"title": user.full_name,
|
"title": user.full_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user