Compare commits
12 Commits
v2.7.0
...
revert-101
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cdd155f75 | ||
|
|
de8c907c51 | ||
|
|
0fd1cabd60 | ||
|
|
8dd480735c | ||
|
|
676f1a1f0e | ||
|
|
ce75422126 | ||
|
|
3a097d6b15 | ||
|
|
9de1bf1020 | ||
|
|
93e5cf1c25 | ||
|
|
6e2376570b | ||
|
|
b20c4bf197 | ||
|
|
6ae1d92033 |
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/frappe/frappe-ui
|
url = https://github.com/pateljannat/frappe-ui
|
||||||
|
|||||||
@@ -31,35 +31,12 @@ describe("Course Creation", () => {
|
|||||||
.contains("Preview Video")
|
.contains("Preview Video")
|
||||||
.type("https://www.youtube.com/embed/-LPmw2Znl2c");
|
.type("https://www.youtube.com/embed/-LPmw2Znl2c");
|
||||||
cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}");
|
cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}");
|
||||||
cy.get("label")
|
cy.get(".search-input").click().type("frappe");
|
||||||
.contains("Category")
|
cy.wait(1000);
|
||||||
.parent()
|
|
||||||
.within(() => {
|
|
||||||
cy.get("button").click();
|
|
||||||
});
|
|
||||||
cy.get("[id^=headlessui-combobox-option-")
|
cy.get("[id^=headlessui-combobox-option-")
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
/* Instructor */
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Instructors")
|
|
||||||
.parent()
|
|
||||||
.within(() => {
|
|
||||||
cy.get("input").click().type("frappe");
|
|
||||||
cy.get("input")
|
|
||||||
.invoke("attr", "aria-controls")
|
|
||||||
.as("instructor_list_id");
|
|
||||||
});
|
|
||||||
cy.get("@instructor_list_id").then((instructor_list_id) => {
|
|
||||||
cy.get(`[id^=${instructor_list_id}`)
|
|
||||||
.should("be.visible")
|
|
||||||
.within(() => {
|
|
||||||
cy.get("[id^=headlessui-combobox-option-").first().click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get("label").contains("Published").click();
|
cy.get("label").contains("Published").click();
|
||||||
cy.get("label").contains("Published On").type("2021-01-01");
|
cy.get("label").contains("Published On").type("2021-01-01");
|
||||||
cy.button("Save").click();
|
cy.button("Save").click();
|
||||||
@@ -84,7 +61,21 @@ 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."
|
||||||
);
|
);
|
||||||
@@ -128,6 +119,12 @@ 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."
|
||||||
);
|
);
|
||||||
|
|||||||
Submodule frappe-ui deleted from 8cd9b06a5e
@@ -19,10 +19,9 @@
|
|||||||
"@editorjs/paragraph": "^2.11.3",
|
"@editorjs/paragraph": "^2.11.3",
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
"@editorjs/simple-image": "^1.6.0",
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"codemirror-editor-vue3": "^2.8.0",
|
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.69",
|
"frappe-ui": "^0.1.56",
|
||||||
"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",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
frontend/public/Youtube.mov
Normal file
BIN
frontend/public/Youtube.mov
Normal file
Binary file not shown.
Binary file not shown.
@@ -14,10 +14,8 @@ import DesktopLayout from './components/DesktopLayout.vue'
|
|||||||
import MobileLayout from './components/MobileLayout.vue'
|
import MobileLayout from './components/MobileLayout.vue'
|
||||||
import { stopSession } from '@/telemetry'
|
import { stopSession } from '@/telemetry'
|
||||||
import { init as initTelemetry } from '@/telemetry'
|
import { init as initTelemetry } from '@/telemetry'
|
||||||
import { usersStore } from '@/stores/user'
|
|
||||||
|
|
||||||
const screenSize = useScreenSize()
|
const screenSize = useScreenSize()
|
||||||
let { userResource } = usersStore()
|
|
||||||
|
|
||||||
const Layout = computed(() => {
|
const Layout = computed(() => {
|
||||||
if (screenSize.width < 640) {
|
if (screenSize.width < 640) {
|
||||||
@@ -28,7 +26,6 @@ const Layout = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!userResource.data) return
|
|
||||||
await initTelemetry()
|
await initTelemetry()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="text-lg font-semibold mb-4">
|
||||||
<div class="text-lg font-semibold mb-4">
|
{{ __('Assessments') }}
|
||||||
{{ __('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
|
||||||
@@ -17,76 +9,41 @@
|
|||||||
: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 {
|
import { ListView, createResource } from 'frappe-ui'
|
||||||
ListView,
|
import { inject } from 'vue'
|
||||||
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: {
|
||||||
@@ -117,61 +74,6 @@ 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,11 +4,15 @@
|
|||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ __('Courses') }}
|
{{ __('Courses') }}
|
||||||
</div>
|
</div>
|
||||||
<Button v-if="canSeeAddButton()" @click="openCourseModal()">
|
<Button
|
||||||
|
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') }}
|
{{ __('Add Course') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="courses.data?.length">
|
<div v-if="courses.data?.length">
|
||||||
@@ -84,7 +88,6 @@ 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')
|
||||||
@@ -129,32 +132,23 @@ const getCoursesColumns = () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCourses = createResource({
|
const removeCourse = createResource({
|
||||||
url: 'lms.lms.api.delete_documents',
|
url: 'frappe.client.delete',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: 'Batch Course',
|
doctype: 'Batch Course',
|
||||||
documents: values.courses,
|
name: values.course,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeCourses = (selections, unselectAll) => {
|
const removeCourses = (selections, unselectAll) => {
|
||||||
deleteCourses.submit(
|
selections.forEach(async (course) => {
|
||||||
{
|
removeCourse.submit({ course })
|
||||||
courses: Array.from(selections),
|
})
|
||||||
},
|
setTimeout(() => {
|
||||||
{
|
courses.reload()
|
||||||
onSuccess(data) {
|
unselectAll()
|
||||||
courses.reload()
|
}, 1000)
|
||||||
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" @click="openStudentModal()">
|
<Button class="float-right mb-3" variant="solid" @click="openStudentModal()">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
{{ __('Add') }}
|
{{ __('Add Student') }}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Students') }}
|
{{ __('Students') }}
|
||||||
@@ -88,7 +88,6 @@ 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)
|
||||||
|
|
||||||
@@ -136,28 +135,23 @@ const openStudentModal = () => {
|
|||||||
showStudentModal.value = true
|
showStudentModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteStudents = createResource({
|
const removeStudent = createResource({
|
||||||
url: 'lms.lms.api.delete_documents',
|
url: 'frappe.client.delete',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: 'Batch Student',
|
doctype: 'Batch Student',
|
||||||
documents: values.students,
|
name: values.student,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeStudents = (selections, unselectAll) => {
|
const removeStudents = (selections, unselectAll) => {
|
||||||
deleteStudents.submit(
|
selections.forEach(async (student) => {
|
||||||
{
|
removeStudent.submit({ student })
|
||||||
students: Array.from(selections),
|
})
|
||||||
},
|
setTimeout(() => {
|
||||||
{
|
students.reload()
|
||||||
onSuccess(data) {
|
unselectAll()
|
||||||
students.reload()
|
}, 500)
|
||||||
showToast(__('Success'), __('Students deleted successfully'), 'check')
|
|
||||||
unselectAll()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col justify-between h-full">
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="font-semibold mb-1">
|
|
||||||
{{ __(label) }}
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
v-if="isDirty"
|
|
||||||
:label="__('Not Saved')"
|
|
||||||
variant="subtle"
|
|
||||||
theme="orange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-600">
|
|
||||||
{{ __(description) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SettingFields :fields="fields" :data="data.data" />
|
|
||||||
<div class="flex flex-row-reverse mt-auto">
|
|
||||||
<Button variant="solid" :loading="saveSettings.loading" @click="update">
|
|
||||||
{{ __('Update') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { createResource, Button, Badge } from 'frappe-ui'
|
|
||||||
import SettingFields from '@/components/SettingFields.vue'
|
|
||||||
import { watch, ref } from 'vue'
|
|
||||||
|
|
||||||
const isDirty = ref(false)
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
fields: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const saveSettings = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Website Settings',
|
|
||||||
name: 'Website Settings',
|
|
||||||
fieldname: values.fields,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
let fieldsToSave = {}
|
|
||||||
let imageFields = ['favicon', 'banner_image', 'footer_logo']
|
|
||||||
props.fields.forEach((f) => {
|
|
||||||
if (imageFields.includes(f.name)) {
|
|
||||||
fieldsToSave[f.name] = f.value ? f.value.file_url : null
|
|
||||||
} else {
|
|
||||||
fieldsToSave[f.name] = f.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
saveSettings.submit({
|
|
||||||
fields: fieldsToSave,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(props.data, (newData) => {
|
|
||||||
if (newData && !isDirty.value) {
|
|
||||||
isDirty.value = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col min-h-0">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-xl font-semibold mb-1">
|
|
||||||
{{ label }}
|
|
||||||
</div>
|
|
||||||
<Button @click="() => showCategoryForm()">
|
|
||||||
<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
|
|
||||||
v-if="showForm"
|
|
||||||
class="flex items-center justify-between my-4 space-x-2"
|
|
||||||
>
|
|
||||||
<FormControl
|
|
||||||
ref="categoryInput"
|
|
||||||
v-model="category"
|
|
||||||
:placeholder="__('Category Name')"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<Button @click="addCategory()" variant="subtle">
|
|
||||||
{{ __('Add') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-scroll">
|
|
||||||
<div class="text-base divide-y">
|
|
||||||
<FormControl
|
|
||||||
:value="cat.category"
|
|
||||||
type="text"
|
|
||||||
v-for="cat in categories.data"
|
|
||||||
class="form-control"
|
|
||||||
@change.stop="(e) => update(cat.name, e.target.value)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
createListResource,
|
|
||||||
createResource,
|
|
||||||
debounce,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { Plus, X } from 'lucide-vue-next'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const showForm = ref(false)
|
|
||||||
const category = ref(null)
|
|
||||||
const categoryInput = ref(null)
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const categories = createListResource({
|
|
||||||
doctype: 'LMS Category',
|
|
||||||
fields: ['name', 'category'],
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const newCategory = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'LMS Category',
|
|
||||||
category: category.value,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const addCategory = () => {
|
|
||||||
newCategory.submit(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onSuccess(data) {
|
|
||||||
categories.reload()
|
|
||||||
category.value = null
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showCategoryForm = () => {
|
|
||||||
showForm.value = !showForm.value
|
|
||||||
setTimeout(() => {
|
|
||||||
categoryInput.value.$el.querySelector('input').focus()
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCategory = createResource({
|
|
||||||
url: 'frappe.client.rename_doc',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Category',
|
|
||||||
old_name: values.name,
|
|
||||||
new_name: values.category,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const update = (name, value) => {
|
|
||||||
updateCategory.submit(
|
|
||||||
{
|
|
||||||
name: name,
|
|
||||||
category: value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
categories.reload()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.form-control input {
|
|
||||||
padding: 1.25rem 0;
|
|
||||||
border-color: transparent;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control input:focus {
|
|
||||||
outline: transparent;
|
|
||||||
background: white;
|
|
||||||
box-shadow: none;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control input:hover {
|
|
||||||
outline: transparent;
|
|
||||||
background: white;
|
|
||||||
box-shadow: none;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -108,7 +108,6 @@ 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,27 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-1">
|
<div class="flex text-center">
|
||||||
<label class="block text-xs text-gray-600" v-if="props.label">
|
<div v-for="index in 5">
|
||||||
{{ props.label }}
|
<Star
|
||||||
</label>
|
:class="index <= rating ? 'fill-orange-500' : ''"
|
||||||
<div class="flex text-center">
|
class="h-6 w-6 fill-gray-400 text-gray-50 mr-1 cursor-pointer"
|
||||||
<div
|
@click="markRating(index)"
|
||||||
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 { computed, ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
@@ -32,36 +23,10 @@ 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'])
|
||||||
const rating = ref(props.modelValue)
|
let rating = ref(props.modelValue)
|
||||||
const hoveredRating = ref(0)
|
|
||||||
|
|
||||||
let emitChange = (value) => {
|
let emitChange = (value) => {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
@@ -71,11 +36,4 @@ function markRating(index) {
|
|||||||
emitChange(index)
|
emitChange(index)
|
||||||
rating.value = index
|
rating.value = index
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newVal) => {
|
|
||||||
rating.value = newVal
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ 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()
|
||||||
@@ -156,9 +155,6 @@ 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 leading-5">
|
<div class="font-semibold text-lg">
|
||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
<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>
|
|
||||||
174
frontend/src/components/LessonPlugins.vue
Normal file
174
frontend/src/components/LessonPlugins.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<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>
|
||||||
@@ -1,30 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between mb-5">
|
<Button
|
||||||
<div class="text-lg font-semibold">
|
v-if="user.data.is_moderator"
|
||||||
{{ __('Live Class') }}
|
variant="solid"
|
||||||
</div>
|
class="float-right mb-5"
|
||||||
<Button v-if="user.data.is_moderator" @click="openLiveClassModal">
|
@click="openLiveClassModal"
|
||||||
<template #prefix>
|
>
|
||||||
<Plus class="h-4 w-4" />
|
<template #prefix>
|
||||||
</template>
|
<Plus class="h-4 w-4" />
|
||||||
<span>
|
</template>
|
||||||
{{ __('Add') }}
|
<span>
|
||||||
</span>
|
{{ __('Add Live Class') }}
|
||||||
</Button>
|
</span>
|
||||||
|
</Button>
|
||||||
|
<div class="text-lg font-semibold mb-5">
|
||||||
|
{{ __('Live Class') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="liveClasses.data?.length" class="grid grid-cols-2 gap-5">
|
<div v-if="liveClasses.data?.length" class="grid grid-cols-2 gap-5">
|
||||||
<div
|
<div
|
||||||
v-for="cls in liveClasses.data"
|
v-for="cls in liveClasses.data"
|
||||||
class="flex flex-col border rounded-md h-full text-sm text-gray-700 p-3"
|
class="flex flex-col border rounded-md h-full p-3"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-gray-900 text-lg mb-4">
|
<div class="font-semibold text-lg mb-4">
|
||||||
{{ cls.title }}
|
{{ cls.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="leading-5 text-gray-700 text-sm mb-4">
|
<div class="mb-4">
|
||||||
{{ cls.description }}
|
{{ cls.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<Calendar class="w-4 h-4 stroke-1.5 text-gray-700" />
|
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
|
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -35,7 +38,7 @@
|
|||||||
{{ formatTime(cls.time) }}
|
{{ formatTime(cls.time) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 text-gray-900 mt-auto">
|
<div class="flex items-center space-x-2 mt-auto">
|
||||||
<a
|
<a
|
||||||
:href="cls.start_url"
|
:href="cls.start_url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -87,6 +90,7 @@ const liveClasses = createListResource({
|
|||||||
doctype: 'LMS Live Class',
|
doctype: 'LMS Live Class',
|
||||||
filters: {
|
filters: {
|
||||||
batch_name: props.batch,
|
batch_name: props.batch,
|
||||||
|
date: ['>=', new Date()],
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
'title',
|
'title',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-0 flex-col text-base">
|
<div class="text-base p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold mb-1">
|
<div class="font-semibold mb-1">
|
||||||
{{ __(label) }}
|
{{ __(label) }}
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
</div> -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex item-center space-x-2">
|
<div class="flex item-center space-x-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -16,93 +16,74 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:debounce="300"
|
:debounce="300"
|
||||||
/>
|
/>
|
||||||
<Button @click="() => (showForm = !showForm)">
|
<Button @click="() => (showForm = true)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
<Plus class="h-3 w-3 stroke-1.5" />
|
||||||
<X v-else class="h-3 w-3 stroke-1.5" />
|
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="my-4">
|
||||||
|
<!-- Form to add new member -->
|
||||||
|
<div v-if="showForm" class="flex items-center space-x-2 mb-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>
|
||||||
|
|
||||||
<!-- 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 -->
|
<!-- 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
|
<div
|
||||||
v-if="memberList.length && hasNextPage"
|
v-for="member in memberList"
|
||||||
class="flex justify-center mt-4"
|
class="grid grid-cols-5 grid-flow-row py-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
<Button @click="members.reload()">
|
<div
|
||||||
<template #prefix>
|
@click="openProfile(member.username)"
|
||||||
<RefreshCw class="h-3 w-3 stroke-1.5" />
|
class="flex items-center space-x-2 col-span-2"
|
||||||
</template>
|
>
|
||||||
{{ __('Load More') }}
|
<Avatar
|
||||||
</Button>
|
:image="member.user_image"
|
||||||
|
:label="member.full_name"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{{ member.full_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-700 col-span-2">
|
||||||
|
{{ member.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-700 justify-self-end">
|
||||||
|
{{ getRole(member.role) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hasNextPage" class="flex justify-center">
|
||||||
|
<Button variant="solid" @click="members.reload()">
|
||||||
|
<template #prefix>
|
||||||
|
<RefreshCw class="h-3 w-3 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
|
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ref, watch, reactive, inject } from 'vue'
|
import { ref, watch, reactive } from 'vue'
|
||||||
import { RefreshCw, Plus, X } from 'lucide-vue-next'
|
import { RefreshCw, Plus } from 'lucide-vue-next'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const show = defineModel('show')
|
const show = defineModel('show')
|
||||||
@@ -111,7 +92,6 @@ const start = ref(0)
|
|||||||
const memberList = ref([])
|
const memberList = ref([])
|
||||||
const hasNextPage = ref(false)
|
const hasNextPage = ref(false)
|
||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const dayjs = inject('$dayjs')
|
|
||||||
|
|
||||||
const member = reactive({
|
const member = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
<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,24 +15,18 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<FormControl
|
<FormControl label="Title" v-model="chapter.title" class="mb-4" />
|
||||||
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, ref } from 'vue'
|
import { defineModel, reactive, watch } 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: {
|
||||||
@@ -43,7 +37,6 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const chapter = reactive({
|
const chapter = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
})
|
})
|
||||||
@@ -104,7 +97,6 @@ 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',
|
||||||
@@ -168,12 +160,4 @@ watch(
|
|||||||
chapter.title = newChapter?.title
|
chapter.title = newChapter?.title
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(show, () => {
|
|
||||||
if (show.value) {
|
|
||||||
setTimeout(() => {
|
|
||||||
chapterInput.value.$el.querySelector('input').focus()
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -131,16 +131,10 @@ function submitEvaluation(close) {
|
|||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
let message = err.messages?.[0] || err
|
let message = err.messages?.[0] || err
|
||||||
let unavailabilityMessage
|
let unavailabilityMessage = message.includes('unavailable')
|
||||||
|
|
||||||
if (typeof message === 'string') {
|
|
||||||
unavailabilityMessage = message?.includes('unavailable')
|
|
||||||
} else {
|
|
||||||
unavailabilityMessage = false
|
|
||||||
}
|
|
||||||
|
|
||||||
createToast({
|
createToast({
|
||||||
title: unavailabilityMessage ? __('Evaluator is Unavailable') : '',
|
title: unavailabilityMessage ? 'Evaluator is Unavailable' : 'Error',
|
||||||
text: message,
|
text: message,
|
||||||
icon: unavailabilityMessage ? 'alert-circle' : 'x',
|
icon: unavailabilityMessage ? 'alert-circle' : 'x',
|
||||||
iconClasses: 'bg-yellow-600 text-white rounded-md p-px',
|
iconClasses: 'bg-yellow-600 text-white rounded-md p-px',
|
||||||
|
|||||||
@@ -1,378 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<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 '/assets/lms/frontend/Youtube.mp4'
|
|
||||||
if (props.type == 'quiz') return '/assets/lms/frontend/Quiz.mp4'
|
|
||||||
if (props.type == 'upload') return '/assets/lms/frontend/Upload.mp4'
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -212,7 +212,7 @@ const questionCreation = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const submitQuestion = (close) => {
|
const submitQuestion = (close) => {
|
||||||
if (props.questionDetail?.question) updateQuestion(close)
|
if (questionData.data?.name) updateQuestion(close)
|
||||||
else addQuestion(close)
|
else addQuestion(close)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ const addQuestion = (close) => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
showToast(__('Error'), __(err.message?.[0] || err), 'x')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -259,7 +259,7 @@ const addQuestionRow = (question, close) => {
|
|||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
showToast(__('Error'), __(err.message?.[0] || err), 'x')
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -312,12 +312,13 @@ 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,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="show" :options="{ size: '4xl' }">
|
<Dialog v-model="show" :options="{ size: '3xl' }">
|
||||||
<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">
|
||||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
|
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div v-for="tab in tabs" :key="tab.label">
|
<div v-for="tab in tabs">
|
||||||
<div
|
<div
|
||||||
v-if="!tab.hideLabel"
|
v-if="!tab.hideLabel"
|
||||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
|
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
<SidebarLink
|
<SidebarLink
|
||||||
v-for="item in tab.items"
|
v-for="item in tab.items"
|
||||||
:link="item"
|
:link="item"
|
||||||
:key="item.label"
|
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:class="
|
:class="
|
||||||
activeTab?.label == item.label
|
activeTab?.label == item.label
|
||||||
@@ -31,8 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="activeTab && data.doc"
|
v-if="activeTab && data.doc"
|
||||||
:key="activeTab.label"
|
class="flex flex-1 flex-col overflow-y-auto"
|
||||||
class="flex flex-1 flex-col px-10 py-8"
|
|
||||||
>
|
>
|
||||||
<Members
|
<Members
|
||||||
v-if="activeTab.label === 'Members'"
|
v-if="activeTab.label === 'Members'"
|
||||||
@@ -40,25 +38,6 @@
|
|||||||
:description="activeTab.description"
|
:description="activeTab.description"
|
||||||
v-model:show="show"
|
v-model:show="show"
|
||||||
/>
|
/>
|
||||||
<Categories
|
|
||||||
v-else-if="activeTab.label === 'Categories'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
/>
|
|
||||||
<PaymentSettings
|
|
||||||
v-else-if="activeTab.label === 'Payment Gateway'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
:data="data"
|
|
||||||
:fields="activeTab.fields"
|
|
||||||
/>
|
|
||||||
<BrandSettings
|
|
||||||
v-else-if="activeTab.label === 'Branding'"
|
|
||||||
:label="activeTab.label"
|
|
||||||
:description="activeTab.description"
|
|
||||||
:fields="activeTab.fields"
|
|
||||||
:data="branding"
|
|
||||||
/>
|
|
||||||
<SettingDetails
|
<SettingDetails
|
||||||
v-else
|
v-else
|
||||||
:fields="activeTab.fields"
|
:fields="activeTab.fields"
|
||||||
@@ -72,20 +51,15 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createDocumentResource, createResource } from 'frappe-ui'
|
import { Dialog, createDocumentResource } from 'frappe-ui'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useSettings } from '@/stores/settings'
|
|
||||||
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'
|
import Members from '@/components/Members.vue'
|
||||||
import Categories from '@/components/Categories.vue'
|
|
||||||
import BrandSettings from '@/components/BrandSettings.vue'
|
|
||||||
import PaymentSettings from '@/components/PaymentSettings.vue'
|
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const doctype = ref('LMS Settings')
|
const doctype = ref('LMS Settings')
|
||||||
const activeTab = ref(null)
|
const activeTab = ref(null)
|
||||||
const settingsStore = useSettings()
|
|
||||||
|
|
||||||
const data = createDocumentResource({
|
const data = createDocumentResource({
|
||||||
doctype: doctype.value,
|
doctype: doctype.value,
|
||||||
@@ -95,14 +69,8 @@ const data = createDocumentResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const branding = createResource({
|
const tabs = computed(() => {
|
||||||
url: 'lms.lms.api.get_branding',
|
let _tabs = [
|
||||||
auto: true,
|
|
||||||
cache: 'brand',
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabsStructure = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
@@ -112,12 +80,6 @@ const tabsStructure = computed(() => {
|
|||||||
description: 'Manage the members of your learning system',
|
description: 'Manage the members of your learning system',
|
||||||
icon: 'UserRoundPlus',
|
icon: 'UserRoundPlus',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Settings',
|
|
||||||
hideLabel: true,
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
label: 'Payment Gateway',
|
label: 'Payment Gateway',
|
||||||
icon: 'DollarSign',
|
icon: 'DollarSign',
|
||||||
@@ -125,10 +87,14 @@ const tabsStructure = computed(() => {
|
|||||||
'Configure the payment gateway and other payment related settings',
|
'Configure the payment gateway and other payment related settings',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Payment Gateway',
|
label: 'Razorpay Key',
|
||||||
name: 'payment_gateway',
|
name: 'razorpay_key',
|
||||||
type: 'Link',
|
type: 'text',
|
||||||
doctype: 'Payment Gateway',
|
},
|
||||||
|
{
|
||||||
|
label: 'Razorpay Secret',
|
||||||
|
name: 'razorpay_secret',
|
||||||
|
type: 'password',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Default Currency',
|
label: 'Default Currency',
|
||||||
@@ -136,6 +102,9 @@ const tabsStructure = computed(() => {
|
|||||||
type: 'Link',
|
type: 'Link',
|
||||||
doctype: 'Currency',
|
doctype: 'Currency',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'Column Break',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Apply GST for India',
|
label: 'Apply GST for India',
|
||||||
name: 'apply_gst',
|
name: 'apply_gst',
|
||||||
@@ -159,67 +128,10 @@ const tabsStructure = computed(() => {
|
|||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
label: 'Categories',
|
|
||||||
description: 'Manage the members of your learning system',
|
|
||||||
icon: 'Network',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Customise',
|
|
||||||
hideLabel: false,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Branding',
|
|
||||||
icon: 'Blocks',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: 'Brand Name',
|
|
||||||
name: 'app_name',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Copyright',
|
|
||||||
name: 'copyright',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Address',
|
|
||||||
name: 'address',
|
|
||||||
type: 'textarea',
|
|
||||||
rows: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Footer "Powered By"',
|
|
||||||
name: 'footer_powered',
|
|
||||||
type: 'textarea',
|
|
||||||
rows: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Column Break',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Logo',
|
|
||||||
name: 'banner_image',
|
|
||||||
type: 'Upload',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Favicon',
|
|
||||||
name: 'favicon',
|
|
||||||
type: 'Upload',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Footer Logo',
|
|
||||||
name: 'footer_logo',
|
|
||||||
type: 'Upload',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Sidebar',
|
label: 'Sidebar',
|
||||||
icon: 'PanelLeftIcon',
|
icon: 'PanelLeftIcon',
|
||||||
description: 'Choose the items you want to show in the sidebar',
|
description: 'Customize the sidebar as per your needs',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Courses',
|
label: 'Courses',
|
||||||
@@ -256,9 +168,16 @@ const tabsStructure = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
hideLabel: true,
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
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',
|
||||||
@@ -280,19 +199,56 @@ const tabsStructure = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
hideLabel: true,
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Signup',
|
label: 'Signup',
|
||||||
icon: 'LogIn',
|
icon: 'LogIn',
|
||||||
|
description:
|
||||||
|
'Customize the signup page to inform users about your terms and policies',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Custom Content',
|
label: 'Show terms of use on signup',
|
||||||
name: 'custom_signup_content',
|
name: 'terms_of_use',
|
||||||
type: 'Code',
|
type: 'checkbox',
|
||||||
mode: 'htmlmixed',
|
|
||||||
rows: 10,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ask user category',
|
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',
|
name: 'user_category',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
},
|
},
|
||||||
@@ -301,28 +257,23 @@ const tabsStructure = computed(() => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
|
||||||
|
|
||||||
const tabs = computed(() => {
|
return _tabs.map((tab) => {
|
||||||
return tabsStructure.value.map((tab) => {
|
tab.items = tab.items.filter((item) => {
|
||||||
return {
|
if (item.condition) {
|
||||||
...tab,
|
return item.condition()
|
||||||
items: tab.items.filter((item) => {
|
}
|
||||||
return !item.condition || item.condition()
|
return true
|
||||||
}),
|
})
|
||||||
}
|
return tab
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(show, async () => {
|
watch(show, () => {
|
||||||
if (show.value) {
|
if (show.value) {
|
||||||
const currentTab = await tabs.value
|
activeTab.value = tabs.value[0].items[0]
|
||||||
.flatMap((tab) => tab.items)
|
|
||||||
.find((item) => item.label === settingsStore.activeTab)
|
|
||||||
activeTab.value = currentTab || tabs.value[0].items[0]
|
|
||||||
} else {
|
} else {
|
||||||
activeTab.value = null
|
activeTab.value = null
|
||||||
settingsStore.isSettingsOpen = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
31
frontend/src/components/Modals/YouTubeExplanation.vue
Normal file
31
frontend/src/components/Modals/YouTubeExplanation.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<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 @click="redirectToLogin()" class="mt-4">
|
<Button variant="solid" @click="redirectToLogin()" class="mt-2">
|
||||||
{{ __('Login') }}
|
{{ __('Login') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col h-full">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-xl font-semibold mb-1">
|
|
||||||
{{ label }}
|
|
||||||
</div>
|
|
||||||
<!-- <Badge
|
|
||||||
v-if="isDirty"
|
|
||||||
:label="__('Not Saved')"
|
|
||||||
variant="subtle"
|
|
||||||
theme="orange"
|
|
||||||
/> -->
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-scroll">
|
|
||||||
<div class="flex space-x-4">
|
|
||||||
<SettingFields :fields="fields" :data="data.doc" class="w-1/2" />
|
|
||||||
<SettingFields
|
|
||||||
v-if="paymentGateway.data"
|
|
||||||
:fields="paymentGateway.data.fields"
|
|
||||||
:data="paymentGateway.data.data"
|
|
||||||
class="w-1/2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row-reverse mt-auto">
|
|
||||||
<Button variant="solid" @click="update">
|
|
||||||
{{ __('Update') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import SettingFields from '@/components/SettingFields.vue'
|
|
||||||
import { createResource, Badge, Button } from 'frappe-ui'
|
|
||||||
import { watch, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const paymentGateway = createResource({
|
|
||||||
url: 'lms.lms.api.get_payment_gateway_details',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
payment_gateway: props.data.doc.payment_gateway,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const saveSettings = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
makeParams(values) {
|
|
||||||
let fields = {}
|
|
||||||
Object.keys(paymentGateway.data.data).forEach((key) => {
|
|
||||||
if (
|
|
||||||
paymentGateway.data.data[key] &&
|
|
||||||
typeof paymentGateway.data.data[key] === 'object'
|
|
||||||
) {
|
|
||||||
fields[key] = paymentGateway.data.data[key].file_url
|
|
||||||
} else {
|
|
||||||
fields[key] = paymentGateway.data.data[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
doctype: paymentGateway.data.doctype,
|
|
||||||
name: paymentGateway.data.docname,
|
|
||||||
fieldname: fields,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: false,
|
|
||||||
onSuccess(data) {
|
|
||||||
paymentGateway.reload()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
props.fields.forEach((f) => {
|
|
||||||
if (f.type != 'Column Break') {
|
|
||||||
props.data.doc[f.name] = f.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
props.data.save.submit()
|
|
||||||
saveSettings.submit()
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.data.doc.payment_gateway,
|
|
||||||
() => {
|
|
||||||
paymentGateway.reload()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-gray-900 font-semibold mt-2 leading-5"
|
class="text-gray-900 font-semibold mt-2"
|
||||||
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">
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<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,23 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between h-full p-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex itemsc-center justify-between">
|
<div class="font-semibold mb-1">
|
||||||
<div class="font-semibold mb-1">
|
{{ __(label) }}
|
||||||
{{ __(label) }}
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
v-if="data.isDirty"
|
|
||||||
:label="__('Not Saved')"
|
|
||||||
variant="subtle"
|
|
||||||
theme="orange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex space-x-8 my-5">
|
||||||
<SettingFields :fields="fields" :data="data.doc" />
|
<div v-for="(column, index) in columns" :key="index">
|
||||||
|
<div class="flex flex-col space-y-4 w-60">
|
||||||
|
<div v-for="field in column">
|
||||||
|
<Link
|
||||||
|
v-if="field.type == 'Link'"
|
||||||
|
v-model="field.value"
|
||||||
|
:doctype="field.doctype"
|
||||||
|
:label="field.label"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
:key="field.name"
|
||||||
|
v-model="field.value"
|
||||||
|
:label="field.label"
|
||||||
|
:type="field.type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex flex-row-reverse mt-auto">
|
<div class="flex flex-row-reverse mt-auto">
|
||||||
<Button variant="solid" :loading="data.save.loading" @click="update">
|
<Button variant="solid" :loading="data.save.loading" @click="update">
|
||||||
{{ __('Update') }}
|
{{ __('Update') }}
|
||||||
@@ -27,8 +38,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Badge } from 'frappe-ui'
|
import { FormControl, Button } from 'frappe-ui'
|
||||||
import SettingFields from '@/components/SettingFields.vue'
|
import { computed } from 'vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fields: {
|
fields: {
|
||||||
@@ -48,23 +60,37 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
const cols = []
|
||||||
|
let currentColumn = []
|
||||||
|
|
||||||
|
props.fields.forEach((field) => {
|
||||||
|
if (field.type === 'Column Break') {
|
||||||
|
if (currentColumn.length > 0) {
|
||||||
|
cols.push(currentColumn)
|
||||||
|
currentColumn = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (field.type == 'checkbox') {
|
||||||
|
field.value = props.data.doc[field.name] ? true : false
|
||||||
|
} else {
|
||||||
|
field.value = props.data.doc[field.name]
|
||||||
|
}
|
||||||
|
currentColumn.push(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (currentColumn.length > 0) {
|
||||||
|
cols.push(currentColumn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cols
|
||||||
|
})
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
props.fields.forEach((f) => {
|
props.fields.forEach((f) => {
|
||||||
if (f.type != 'Column Break') {
|
props.data.doc[f.name] = f.value
|
||||||
props.data.doc[f.name] = f.value
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
props.data.save.submit()
|
props.data.save.submit()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.CodeMirror pre.CodeMirror-line,
|
|
||||||
.CodeMirror pre.CodeMirror-line-like {
|
|
||||||
font-family: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="my-5"
|
|
||||||
:class="{ 'flex justify-between w-full': columns.length > 1 }"
|
|
||||||
>
|
|
||||||
<div v-for="(column, index) in columns" :key="index">
|
|
||||||
<div
|
|
||||||
class="flex flex-col space-y-5"
|
|
||||||
:class="columns.length > 1 ? 'w-72' : 'w-full'"
|
|
||||||
>
|
|
||||||
<div v-for="field in column">
|
|
||||||
<Link
|
|
||||||
v-if="field.type == 'Link'"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
:label="__(field.label)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-else-if="field.type == 'Code'">
|
|
||||||
<div>
|
|
||||||
{{ __(field.label) }}
|
|
||||||
</div>
|
|
||||||
<Codemirror
|
|
||||||
v-model:value="data[field.name]"
|
|
||||||
:height="200"
|
|
||||||
:options="{
|
|
||||||
mode: field.mode,
|
|
||||||
theme: 'seti',
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="field.type == 'Upload'">
|
|
||||||
<div class="text-sm text-gray-600 mb-1">
|
|
||||||
{{ __(field.label) }}
|
|
||||||
</div>
|
|
||||||
<FileUploader
|
|
||||||
v-if="!data[field.name]"
|
|
||||||
:fileTypes="['image/*']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => (data[field.name] = file)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
|
||||||
>
|
|
||||||
<div class="">
|
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
|
||||||
{{
|
|
||||||
uploading ? `Uploading ${progress}%` : 'Upload an image'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex items-center text-sm">
|
|
||||||
<div class="border rounded-md p-2 mr-2">
|
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col flex-wrap">
|
|
||||||
<span class="break-all">
|
|
||||||
{{ data[field.name]?.file_name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500 mt-1">
|
|
||||||
{{ getFileSize(data[field.name]?.file_size) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<X
|
|
||||||
@click="data[field.name] = null"
|
|
||||||
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
:key="field.name"
|
|
||||||
v-model="data[field.name]"
|
|
||||||
:label="__(field.label)"
|
|
||||||
:type="field.type"
|
|
||||||
:rows="field.rows"
|
|
||||||
:options="field.options"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { FormControl, FileUploader, Button } from 'frappe-ui'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { getFileSize, validateFile } from '@/utils'
|
|
||||||
import { X, FileText } from 'lucide-vue-next'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import Codemirror from 'codemirror-editor-vue3'
|
|
||||||
import 'codemirror/theme/seti.css'
|
|
||||||
import 'codemirror/mode/htmlmixed/htmlmixed.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
fields: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns = computed(() => {
|
|
||||||
const cols = []
|
|
||||||
let currentColumn = []
|
|
||||||
|
|
||||||
props.fields.forEach((field) => {
|
|
||||||
if (field.type === 'Column Break') {
|
|
||||||
if (currentColumn.length > 0) {
|
|
||||||
cols.push(currentColumn)
|
|
||||||
currentColumn = []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (field.type == 'checkbox') {
|
|
||||||
field.value = props.data[field.name] ? true : false
|
|
||||||
} else {
|
|
||||||
field.value = props.data[field.name]
|
|
||||||
}
|
|
||||||
currentColumn.push(field)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (currentColumn.length > 0) {
|
|
||||||
cols.push(currentColumn)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cols
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
: 'hover:bg-gray-200 px-2 w-52'
|
: 'hover:bg-gray-200 px-2 w-52'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<img
|
<span
|
||||||
v-if="branding.data?.banner_image"
|
v-if="branding.data?.brand_html"
|
||||||
:src="branding.data?.banner_image.file_url"
|
v-html="branding.data?.brand_html"
|
||||||
class="w-8 h-8 rounded flex-shrink-0"
|
class="w-8 h-8 rounded flex-shrink-0"
|
||||||
/>
|
></span>
|
||||||
<LMSLogo v-else class="w-8 h-8 rounded flex-shrink-0" />
|
<LMSLogo v-else class="w-8 h-8 rounded flex-shrink-0" />
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-col text-left duration-300 ease-in-out"
|
class="flex flex-1 flex-col text-left duration-300 ease-in-out"
|
||||||
@@ -28,10 +28,11 @@
|
|||||||
<div class="text-base font-medium text-gray-900 leading-none">
|
<div class="text-base font-medium text-gray-900 leading-none">
|
||||||
<span
|
<span
|
||||||
v-if="
|
v-if="
|
||||||
branding.data?.app_name && branding.data?.app_name != 'Frappe'
|
branding.data?.brand_name &&
|
||||||
|
branding.data?.brand_name != 'Frappe'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ branding.data?.app_name }}
|
{{ branding.data?.brand_name }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else> Learning </span>
|
<span v-else> Learning </span>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,20 +67,25 @@ import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
|||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
import Apps from '@/components/Apps.vue'
|
import Apps from '@/components/Apps.vue'
|
||||||
import { ChevronDown, LogIn, LogOut, User, Settings } from 'lucide-vue-next'
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
LogIn,
|
||||||
|
LogOut,
|
||||||
|
User,
|
||||||
|
ArrowRightLeft,
|
||||||
|
Settings,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
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 { useSettings } from '@/stores/settings'
|
import { ref, markRaw } from 'vue'
|
||||||
import { markRaw, watch, ref } from 'vue'
|
|
||||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
import SettingsModal from '@/components/Modals/Settings.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const showSettingsModal = ref(false)
|
||||||
const { logout, branding } = sessionStore()
|
const { logout, branding } = sessionStore()
|
||||||
let { userResource } = usersStore()
|
let { userResource } = usersStore()
|
||||||
const settingsStore = useSettings()
|
|
||||||
let { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
const showSettingsModal = ref(false)
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isCollapsed: {
|
isCollapsed: {
|
||||||
@@ -88,13 +94,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
() => settingsStore.isSettingsOpen,
|
|
||||||
(value) => {
|
|
||||||
showSettingsModal.value = value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const userDropdownOptions = [
|
const userDropdownOptions = [
|
||||||
{
|
{
|
||||||
icon: User,
|
icon: User,
|
||||||
@@ -119,7 +118,7 @@ const userDropdownOptions = [
|
|||||||
icon: Settings,
|
icon: Settings,
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
settingsStore.isSettingsOpen = true
|
showSettingsModal.value = true
|
||||||
},
|
},
|
||||||
condition: () => {
|
condition: () => {
|
||||||
return userResource.data?.is_moderator
|
return userResource.data?.is_moderator
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
@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>
|
||||||
@@ -72,6 +71,7 @@ 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,9 +13,13 @@
|
|||||||
<div class="text-lg font-semibold mb-4">
|
<div class="text-lg font-semibold mb-4">
|
||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
|
<div class="grid grid-cols-2 gap-10 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<FormControl v-model="batch.title" :label="__('Title')" />
|
<FormControl
|
||||||
|
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
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
:items="[{ label: __('Batches'), route: { name: 'Batches' } }]"
|
:items="[{ label: __('Batches'), route: { name: 'Batches' } }]"
|
||||||
/>
|
/>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<div class="w-44">
|
<div class="w-40">
|
||||||
<Select
|
<Select
|
||||||
v-if="categories.data?.length"
|
v-if="categories.data?.length"
|
||||||
v-model="currentCategory"
|
v-model="currentCategory"
|
||||||
:options="categories.data"
|
:options="categories.data"
|
||||||
:placeholder="__('Category')"
|
:placeholder="__('Filter')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@@ -1,50 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="">
|
||||||
<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
|
|
||||||
class="h-7"
|
|
||||||
:items="[{ label: __('Billing Details'), route: { name: 'Billing' } }]"
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
<div
|
<div
|
||||||
v-if="access.data?.access && orderSummary.data"
|
v-if="access.data?.access && orderSummary.data"
|
||||||
class="pt-5 pb-10 mx-5"
|
class="mt-10 w-1/2 mx-auto"
|
||||||
>
|
>
|
||||||
<!-- <div class="mb-5">
|
<div class="text-3xl font-bold">
|
||||||
<div class="text-lg font-semibold">
|
{{ __('Billing Details') }}
|
||||||
{{ __('Address') }}
|
</div>
|
||||||
|
<div class="text-gray-600 mt-1">
|
||||||
|
{{ __('Enter the billing information to complete the payment.') }}
|
||||||
|
</div>
|
||||||
|
<div class="border rounded-md p-5 mt-5">
|
||||||
|
<div class="text-xl font-semibold">
|
||||||
|
{{ __('Summary') }}
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
<div class="text-gray-600 mt-1">
|
||||||
<div class="flex flex-col lg:flex-row justify-between">
|
{{ __('Review the details of your purchase.') }}
|
||||||
<div
|
</div>
|
||||||
class="h-fit bg-gray-100 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 text-sm font-medium lg:w-1/4"
|
<div class="mt-5">
|
||||||
>
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center justify-between space-x-2">
|
<div>
|
||||||
<div class="text-gray-600">
|
|
||||||
{{ __('Ordered Item') }}
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
{{ orderSummary.data.title }}
|
{{ orderSummary.data.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
:class="{
|
||||||
v-if="orderSummary.data.gst_applied"
|
'font-semibold text-xl': !orderSummary.data.gst_applied,
|
||||||
class="flex items-center justify-between"
|
}"
|
||||||
>
|
>
|
||||||
<div class="text-gray-600">
|
{{
|
||||||
{{ __('Original Amount') }}
|
orderSummary.data.gst_applied
|
||||||
</div>
|
? orderSummary.data.original_amount_formatted
|
||||||
<div class="">
|
: orderSummary.data.total_amount_formatted
|
||||||
{{ orderSummary.data.original_amount_formatted }}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="orderSummary.data.gst_applied"
|
v-if="orderSummary.data.gst_applied"
|
||||||
class="flex items-center justify-between mt-2"
|
class="flex items-center justify-between mt-2"
|
||||||
>
|
>
|
||||||
<div class="text-gray-600">
|
<div>
|
||||||
{{ __('GST Amount') }}
|
{{ __('GST Amount') }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -52,80 +46,107 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between border-t border-gray-400 pt-4 mt-2"
|
v-if="orderSummary.data.gst_applied"
|
||||||
|
class="flex items-center justify-between mt-2"
|
||||||
>
|
>
|
||||||
<div class="text-lg font-semibold">
|
<div>
|
||||||
{{ __('Total') }}
|
{{ __('Total Amount') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-lg font-semibold">
|
<div class="font-semibold text-2xl">
|
||||||
{{ orderSummary.data.total_amount_formatted }}
|
{{ orderSummary.data.total_amount_formatted }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 lg:mr-10">
|
<div class="text-xl font-semibold mt-10">
|
||||||
<div class="mb-5">
|
{{ __('Address') }}
|
||||||
<div class="text-lg font-semibold">
|
</div>
|
||||||
{{ __('Address') }}
|
<div class="text-gray-600 mt-1">
|
||||||
|
{{ __('Specify your billing address correctly.') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-5 mt-4">
|
||||||
|
<div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Billing Name') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.billing_name" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Address Line 1') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.address_line1" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Address Line 2') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.address_line2" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('City') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.city" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('State') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.state" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div>
|
||||||
<div class="space-y-4">
|
<div class="mt-4">
|
||||||
<FormControl
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
:label="__('Billing Name')"
|
{{ __('Country') }}
|
||||||
v-model="billingDetails.billing_name"
|
</div>
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
:label="__('Address Line 1')"
|
|
||||||
v-model="billingDetails.address_line1"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
:label="__('Address Line 2')"
|
|
||||||
v-model="billingDetails.address_line2"
|
|
||||||
/>
|
|
||||||
<FormControl :label="__('City')" v-model="billingDetails.city" />
|
|
||||||
<FormControl
|
|
||||||
:label="__('State')"
|
|
||||||
v-model="billingDetails.state"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<Link
|
<Link
|
||||||
doctype="Country"
|
doctype="Country"
|
||||||
:value="billingDetails.country"
|
:value="billingDetails.country"
|
||||||
@change="(option) => changeCurrency(option)"
|
@change="(option) => changeCurrency(option)"
|
||||||
:label="__('Country')"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
:label="__('Postal Code')"
|
|
||||||
v-model="billingDetails.pincode"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
:label="__('Phone Number')"
|
|
||||||
v-model="billingDetails.phone"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Postal Code') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.pincode" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Phone Number') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.phone" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Source') }}
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
doctype="LMS Source"
|
doctype="LMS Source"
|
||||||
:value="billingDetails.source"
|
:value="billingDetails.source"
|
||||||
@change="(option) => (billingDetails.source = option)"
|
@change="(option) => (billingDetails.source = option)"
|
||||||
:label="__('Where did you hear about us?')"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-if="billingDetails.country == 'India'"
|
|
||||||
:label="__('GST Number')"
|
|
||||||
v-model="billingDetails.gstin"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-if="billingDetails.country == 'India'"
|
|
||||||
:label="__('Pan Number')"
|
|
||||||
v-model="billingDetails.pan"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="billingDetails.country == 'India'" class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('GST Number') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.gstin" />
|
||||||
|
</div>
|
||||||
|
<div v-if="billingDetails.country == 'India'" class="mt-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-700">
|
||||||
|
{{ __('Pan Number') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="billingDetails.pan" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="solid" class="mt-8" @click="generatePaymentLink()">
|
|
||||||
{{ __('Proceed to Payment') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button variant="solid" class="mt-8" @click="generatePaymentLink()">
|
||||||
|
{{ __('Proceed to Payment') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="access.data?.message">
|
<div v-else-if="access.data?.message">
|
||||||
@@ -146,18 +167,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { Input, Button, createResource } from 'frappe-ui'
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
createResource,
|
|
||||||
FormControl,
|
|
||||||
Breadcrumbs,
|
|
||||||
Tooltip,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { reactive, inject, onMounted, ref } from 'vue'
|
import { reactive, inject, onMounted, ref } from 'vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import NotPermitted from '@/components/NotPermitted.vue'
|
import NotPermitted from '@/components/NotPermitted.vue'
|
||||||
import { showToast } from '@/utils/'
|
import { createToast } from '@/utils/'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|
||||||
@@ -188,8 +202,8 @@ const access = createResource({
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
setBillingDetails(data.address)
|
|
||||||
orderSummary.submit()
|
orderSummary.submit()
|
||||||
|
setBillingDetails(data.address)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -210,49 +224,84 @@ const orderSummary = createResource({
|
|||||||
const billingDetails = reactive({})
|
const billingDetails = reactive({})
|
||||||
|
|
||||||
const setBillingDetails = (data) => {
|
const setBillingDetails = (data) => {
|
||||||
billingDetails.billing_name = data?.billing_name || ''
|
billingDetails.billing_name = data.billing_name || ''
|
||||||
billingDetails.address_line1 = data?.address_line1 || ''
|
billingDetails.address_line1 = data.address_line1 || ''
|
||||||
billingDetails.address_line2 = data?.address_line2 || ''
|
billingDetails.address_line2 = data.address_line2 || ''
|
||||||
billingDetails.city = data?.city || ''
|
billingDetails.city = data.city || ''
|
||||||
billingDetails.state = data?.state || ''
|
billingDetails.state = data.state || ''
|
||||||
billingDetails.country = data?.country || ''
|
billingDetails.country = data.country || ''
|
||||||
billingDetails.pincode = data?.pincode || ''
|
billingDetails.pincode = data.pincode || ''
|
||||||
billingDetails.phone = data?.phone || ''
|
billingDetails.phone = data.phone || ''
|
||||||
billingDetails.source = data?.source || ''
|
billingDetails.source = data.source || ''
|
||||||
billingDetails.gstin = data?.gstin || ''
|
billingDetails.gstin = data.gstin || ''
|
||||||
billingDetails.pan = data?.pan || ''
|
billingDetails.pan = data.pan || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentLink = createResource({
|
const paymentOptions = createResource({
|
||||||
url: 'lms.lms.payments.get_payment_link',
|
url: 'lms.lms.utils.get_payment_options',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
||||||
docname: props.name,
|
docname: props.name,
|
||||||
title: orderSummary.data.title,
|
phone: billingDetails.phone,
|
||||||
amount: orderSummary.data.original_amount,
|
country: billingDetails.country,
|
||||||
total_amount: orderSummary.data.amount,
|
|
||||||
currency: orderSummary.data.currency,
|
|
||||||
address: billingDetails,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const generatePaymentLink = () => {
|
const generatePaymentLink = () => {
|
||||||
paymentLink.submit(
|
paymentOptions.submit(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
validate() {
|
validate(params) {
|
||||||
if (!billingDetails.source) {
|
|
||||||
return __('Please let us know where you heard about us from.')
|
|
||||||
}
|
|
||||||
return validateAddress()
|
return validateAddress()
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
window.location.href = data
|
data.handler = (response) => {
|
||||||
|
let doctype = props.type == 'course' ? 'LMS Course' : 'LMS Batch'
|
||||||
|
let docname = props.name
|
||||||
|
handleSuccess(response, doctype, docname, data.order_id)
|
||||||
|
}
|
||||||
|
let rzp1 = new Razorpay(data)
|
||||||
|
rzp1.open()
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast(__('Error'), err.messages?.[0] || err, 'x')
|
showError(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentResource = createResource({
|
||||||
|
url: 'lms.lms.utils.verify_payment',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
response: values.response,
|
||||||
|
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
|
||||||
|
docname: props.name,
|
||||||
|
address: billingDetails,
|
||||||
|
order_id: values.orderId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSuccess = (response, doctype, docname, orderId) => {
|
||||||
|
paymentResource.submit(
|
||||||
|
{
|
||||||
|
response: response,
|
||||||
|
orderId: orderId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
createToast({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Payment Successful',
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = data
|
||||||
|
}, 3000)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,14 +109,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 mb-4">
|
|
||||||
<Link
|
|
||||||
doctype="LMS Category"
|
|
||||||
v-model="course.category"
|
|
||||||
:label="__('Category')"
|
|
||||||
:onCreate="(value, close) => openSettings(close)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="instructors"
|
v-model="instructors"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
@@ -229,20 +221,18 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
getFileSize,
|
getFileSize,
|
||||||
updateDocumentTitle,
|
updateDocumentTitle,
|
||||||
} from '@/utils'
|
} from '../utils'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { useSettings } from '@/stores/settings'
|
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const newTag = ref('')
|
const newTag = ref('')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const instructors = ref([])
|
const instructors = ref([])
|
||||||
const settingsStore = useSettings()
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
@@ -430,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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,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.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,12 +463,6 @@ const removeImage = () => {
|
|||||||
course.course_image = null
|
course.course_image = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSettings = (close) => {
|
|
||||||
close()
|
|
||||||
settingsStore.activeTab = 'Categories'
|
|
||||||
settingsStore.isSettingsOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const check_permission = () => {
|
const check_permission = () => {
|
||||||
let user_is_instructor = false
|
let user_is_instructor = false
|
||||||
if (user.data?.is_moderator) return
|
if (user.data?.is_moderator) return
|
||||||
|
|||||||
@@ -8,15 +8,6 @@
|
|||||||
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
|
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
|
||||||
/>
|
/>
|
||||||
<div class="flex space-x-2 justify-end">
|
<div class="flex space-x-2 justify-end">
|
||||||
<div class="w-44">
|
|
||||||
<FormControl
|
|
||||||
v-if="categories.data?.length"
|
|
||||||
type="select"
|
|
||||||
v-model="currentCategory"
|
|
||||||
:options="categories.data"
|
|
||||||
:placeholder="__('Category')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="w-36">
|
<div class="w-36">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
@@ -128,19 +119,11 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
import { Plus, Search } from 'lucide-vue-next'
|
import { Plus, Search } from 'lucide-vue-next'
|
||||||
import { ref, computed, inject, onMounted, watch } from 'vue'
|
import { ref, computed, inject } from 'vue'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
import { updateDocumentTitle } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const currentCategory = ref(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
let queries = new URLSearchParams(location.search)
|
|
||||||
if (queries.has('category')) {
|
|
||||||
currentCategory.value = queries.get('category')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const courses = createResource({
|
const courses = createResource({
|
||||||
url: 'lms.lms.utils.get_courses',
|
url: 'lms.lms.utils.get_courses',
|
||||||
@@ -185,57 +168,18 @@ const addToTabs = (label) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getCourses = (type) => {
|
const getCourses = (type) => {
|
||||||
let courseList = courses.data[type]
|
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
let query = searchQuery.value.toLowerCase()
|
let query = searchQuery.value.toLowerCase()
|
||||||
courseList = courseList.filter(
|
return courses.data[type].filter(
|
||||||
(course) =>
|
(course) =>
|
||||||
course.title.toLowerCase().includes(query) ||
|
course.title.toLowerCase().includes(query) ||
|
||||||
course.short_introduction.toLowerCase().includes(query) ||
|
course.short_introduction.toLowerCase().includes(query) ||
|
||||||
course.tags.filter((tag) => tag.toLowerCase().includes(query)).length
|
course.tags.filter((tag) => tag.toLowerCase().includes(query)).length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (currentCategory.value && currentCategory.value != '') {
|
return courses.data[type]
|
||||||
courseList = courseList.filter(
|
|
||||||
(course) => course.category == currentCategory.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return courseList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = createResource({
|
|
||||||
url: 'lms.lms.api.get_categories',
|
|
||||||
makeParams() {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Course',
|
|
||||||
filters: {
|
|
||||||
published: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cache: ['courseCategories'],
|
|
||||||
auto: true,
|
|
||||||
transform(data) {
|
|
||||||
data.unshift({
|
|
||||||
label: '',
|
|
||||||
value: null,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => currentCategory.value,
|
|
||||||
() => {
|
|
||||||
let queries = new URLSearchParams(location.search)
|
|
||||||
if (currentCategory.value) {
|
|
||||||
queries.set('category', currentCategory.value)
|
|
||||||
} else {
|
|
||||||
queries.delete('category')
|
|
||||||
}
|
|
||||||
history.pushState(null, '', `${location.pathname}?${queries.toString()}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const pageMeta = computed(() => {
|
const pageMeta = computed(() => {
|
||||||
return {
|
return {
|
||||||
title: 'Courses',
|
title: 'Courses',
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
|
lesson.data.instructor_content?.blocks?.length &&
|
||||||
allowInstructorContent()
|
allowInstructorContent()
|
||||||
"
|
"
|
||||||
class="bg-gray-100 p-3 rounded-md mt-6"
|
class="bg-gray-100 p-3 rounded-md mt-6"
|
||||||
@@ -244,10 +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 (
|
if (data.instructor_content?.blocks?.length)
|
||||||
data.instructor_content &&
|
|
||||||
JSON.parse(data.instructor_content)?.blocks?.length > 1
|
|
||||||
)
|
|
||||||
instructorEditor.value = renderEditor(
|
instructorEditor.value = renderEditor(
|
||||||
'instructor-content',
|
'instructor-content',
|
||||||
data.instructor_content
|
data.instructor_content
|
||||||
@@ -451,10 +448,6 @@ 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;
|
||||||
@@ -544,13 +537,4 @@ 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">
|
||||||
<LessonHelp />
|
<LessonPlugins :editor="editor" :notesEditor="instructorEditor" />
|
||||||
</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 LessonHelp from '@/components/LessonHelp.vue'
|
import LessonPlugins from '@/components/LessonPlugins.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(true),
|
tools: getEditorTools(),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -143,9 +143,7 @@ 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?.lesson?.include_in_preview
|
lesson.include_in_preview = data.include_in_preview ? true : false
|
||||||
? true
|
|
||||||
: false
|
|
||||||
addLessonContent(data)
|
addLessonContent(data)
|
||||||
addInstructorNotes(data)
|
addInstructorNotes(data)
|
||||||
enableAutoSave()
|
enableAutoSave()
|
||||||
@@ -182,7 +180,7 @@ const addInstructorNotes = (data) => {
|
|||||||
const enableAutoSave = () => {
|
const enableAutoSave = () => {
|
||||||
autoSaveInterval = setInterval(() => {
|
autoSaveInterval = setInterval(() => {
|
||||||
saveLesson()
|
saveLesson()
|
||||||
}, 10000)
|
}, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -425,7 +423,7 @@ const breadcrumbs = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: lessonDetails.data?.course_title,
|
label: lessonDetails.data?.course_title,
|
||||||
route: { name: 'CourseForm', params: { courseName: props.courseName } },
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -475,10 +473,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -551,6 +545,10 @@ updateDocumentTitle(pageMeta)
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codeBoxSelectItem:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.codeBoxSelectedItem {
|
.codeBoxSelectedItem {
|
||||||
background-color: lightblue !important;
|
background-color: lightblue !important;
|
||||||
}
|
}
|
||||||
@@ -568,17 +566,4 @@ 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', 'slots', 'schedule']
|
let sections = ['certificates', 'roles', 'evaluations']
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
if (fragments.includes(section)) {
|
if (fragments.includes(section)) {
|
||||||
activeTab.value = convertToTitleCase(section)
|
activeTab.value = convertToTitleCase(section)
|
||||||
@@ -161,8 +161,7 @@ watchEffect(() => {
|
|||||||
About: { name: 'ProfileAbout' },
|
About: { name: 'ProfileAbout' },
|
||||||
Certificates: { name: 'ProfileCertificates' },
|
Certificates: { name: 'ProfileCertificates' },
|
||||||
Roles: { name: 'ProfileRoles' },
|
Roles: { name: 'ProfileRoles' },
|
||||||
Slots: { name: 'ProfileEvaluator' },
|
Evaluations: { name: 'ProfileEvaluator' },
|
||||||
Schedule: { name: 'ProfileEvaluationSchedule' },
|
|
||||||
}[activeTab.value]
|
}[activeTab.value]
|
||||||
router.push(route)
|
router.push(route)
|
||||||
}
|
}
|
||||||
@@ -186,13 +185,8 @@ 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 (
|
if (isSessionUser() && $user.data?.is_evaluator)
|
||||||
isSessionUser() &&
|
buttons.push({ label: 'Evaluations' })
|
||||||
($user.data?.is_evaluator || $user.data?.is_moderator)
|
|
||||||
) {
|
|
||||||
buttons.push({ label: 'Slots' })
|
|
||||||
buttons.push({ label: 'Schedule' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const shareOnSocial = (badge, medium) => {
|
|||||||
const summary = `I am happy to announce that I earned the ${
|
const summary = `I am happy to announce that I earned the ${
|
||||||
badge.badge
|
badge.badge
|
||||||
} badge on ${dayjs(badge.issued_on).format('DD MMM YYYY')} at ${
|
} badge on ${dayjs(badge.issued_on).format('DD MMM YYYY')} at ${
|
||||||
branding.data?.app_name
|
branding.data?.brand_name
|
||||||
}.`
|
}.`
|
||||||
|
|
||||||
if (medium == 'LinkedIn')
|
if (medium == 'LinkedIn')
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
<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-4 mb-8">
|
<div class="grid grid-cols-3 gap-5 mt-2 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="deleteQuestions(selections, unselectAll)"
|
@click="deleteQuizzes(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, updateDocumentTitle } from '@/utils'
|
import { showToast } 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: 'QuizForm',
|
name: 'QuizCreation',
|
||||||
params: { quizID: data.name },
|
params: { quizID: data.name },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -375,29 +375,24 @@ const openQuestionModal = (question = null) => {
|
|||||||
showQuestionModal.value = true
|
showQuestionModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteQuestionResource = createResource({
|
const deleteQuiz = createResource({
|
||||||
url: 'lms.lms.api.delete_documents',
|
url: 'frappe.client.delete',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
return {
|
return {
|
||||||
doctype: 'LMS Quiz Question',
|
doctype: 'LMS Quiz Question',
|
||||||
documents: values.questions,
|
name: values.quiz,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteQuestions = (selections, unselectAll) => {
|
const deleteQuizzes = (selections, unselectAll) => {
|
||||||
deleteQuestionResource.submit(
|
selections.forEach(async (quiz) => {
|
||||||
{
|
deleteQuiz.submit({ quiz })
|
||||||
questions: Array.from(selections),
|
})
|
||||||
},
|
setTimeout(() => {
|
||||||
{
|
quizDetails.reload()
|
||||||
onSuccess() {
|
unselectAll()
|
||||||
showToast(__('Success'), __('Questions deleted successfully'), 'check')
|
}, 500)
|
||||||
quizDetails.reload()
|
|
||||||
unselectAll()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
@@ -415,18 +410,9 @@ 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: 'QuizForm', params: { quizID: props.quizID } },
|
route: { name: 'QuizCreation', 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>
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<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: 'QuizForm',
|
name: 'QuizCreation',
|
||||||
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: 'QuizForm',
|
name: 'QuizCreation',
|
||||||
params: {
|
params: {
|
||||||
quizID: row.name,
|
quizID: row.name,
|
||||||
},
|
},
|
||||||
@@ -62,7 +62,6 @@ 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()
|
||||||
@@ -124,13 +123,4 @@ const breadcrumbs = computed(() => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const pageMeta = computed(() => {
|
|
||||||
return {
|
|
||||||
title: __('Quizzes'),
|
|
||||||
description: __('List of quizzes'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
updateDocumentTitle(pageMeta)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -79,15 +79,9 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ProfileEvaluator',
|
name: 'ProfileEvaluator',
|
||||||
path: 'slots',
|
path: 'evaluations',
|
||||||
component: () => import('@/pages/ProfileEvaluator.vue'),
|
component: () => import('@/pages/ProfileEvaluator.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'ProfileEvaluationSchedule',
|
|
||||||
path: 'schedule',
|
|
||||||
component: () =>
|
|
||||||
import('@/pages/ProfileEvaluationSchedule.vue'),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -154,14 +148,8 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/quizzes/:quizID',
|
path: '/quizzes/:quizID',
|
||||||
name: 'QuizForm',
|
name: 'QuizCreation',
|
||||||
component: () => import('@/pages/QuizForm.vue'),
|
component: () => import('@/pages/QuizCreation.vue'),
|
||||||
props: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/quiz/:quizID',
|
|
||||||
name: 'Quiz',
|
|
||||||
component: () => import('@/pages/QuizSubmission.vue'),
|
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const sessionStore = defineStore('lms-session', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let user = ref(sessionUser())
|
let user = ref(sessionUser())
|
||||||
if (user.value) {
|
if (user) {
|
||||||
allUsers.reload()
|
allUsers.reload()
|
||||||
}
|
}
|
||||||
const isLoggedIn = computed(() => !!user.value)
|
const isLoggedIn = computed(() => !!user.value)
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
export const useSettings = defineStore('settings', () => {
|
|
||||||
const isSettingsOpen = ref(false)
|
|
||||||
const activeTab = ref(null)
|
|
||||||
|
|
||||||
return {
|
|
||||||
isSettingsOpen,
|
|
||||||
activeTab,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -62,7 +62,7 @@ export class CodeBox {
|
|||||||
|
|
||||||
static get toolbox() {
|
static get toolbox() {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
render: () => h(Code, { size: 18, strokeWidth: 1.5, color: 'black' }),
|
render: () => h(Code, { size: 24, strokeWidth: 2, color: 'black' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
32
frontend/src/utils/customEmbed.js
Normal file
32
frontend/src/utils/customEmbed.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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/atom-one-dark.min.css',
|
'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.18.1/build/styles/dracula.min.css', // Optional
|
||||||
themeName: 'atom-one-dark',
|
themeName: 'atom-one-dark', // Optional
|
||||||
useDefaultTheme: 'dark',
|
useDefaultTheme: 'dark', // Optional. This also determines the background color of the language select drop-down
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
@@ -499,10 +499,3 @@ export function singularize(word) {
|
|||||||
(r) => endings[r]
|
(r) => endings[r]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateFile = (file) => {
|
|
||||||
let extension = file.name.split('.').pop().toLowerCase()
|
|
||||||
if (!['jpg', 'jpeg', 'png', 'webp'].includes(extension)) {
|
|
||||||
return __('Only image file is allowed.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import QuizBlock from '@/components/QuizBlock.vue'
|
import QuizBlock from '@/components/QuizBlock.vue'
|
||||||
import QuizPlugin from '@/components/QuizPlugin.vue'
|
import { createApp } from '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 }) {
|
||||||
@@ -11,31 +9,17 @@ 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 (Object.keys(this.data).length) {
|
if (this.data) {
|
||||||
this.renderQuiz(this.data.quiz)
|
let renderedQuiz = this.renderQuiz(this.data.quiz)
|
||||||
} else {
|
if (!this.readOnly) {
|
||||||
this.renderQuizModal()
|
this.wrapper.innerHTML = renderedQuiz
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.wrapper
|
return this.wrapper
|
||||||
}
|
}
|
||||||
@@ -43,7 +27,7 @@ export class Quiz {
|
|||||||
renderQuiz(quiz) {
|
renderQuiz(quiz) {
|
||||||
if (this.readOnly) {
|
if (this.readOnly) {
|
||||||
const app = createApp(QuizBlock, {
|
const app = createApp(QuizBlock, {
|
||||||
quiz: quiz,
|
quiz: quiz, // Pass quiz content as prop
|
||||||
})
|
})
|
||||||
app.use(translationPlugin)
|
app.use(translationPlugin)
|
||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
@@ -51,23 +35,11 @@ export class Quiz {
|
|||||||
app.mount(this.wrapper)
|
app.mount(this.wrapper)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center mb-2'>
|
return `<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,9 +1,6 @@
|
|||||||
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 UploadPlugin from '@/components/UploadPlugin.vue'
|
import { createApp } from '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 }) {
|
||||||
@@ -11,38 +8,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFile(file) {
|
renderUpload(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,
|
||||||
@@ -68,25 +44,6 @@ 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,
|
||||||
|
|||||||
1585
frontend/yarn.lock
1585
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
__version__ = "2.7.0"
|
__version__ = "2.4.0"
|
||||||
|
|||||||
0
lms/config/__init__.py
Normal file
0
lms/config/__init__.py
Normal file
13
lms/config/desktop.py
Normal file
13
lms/config/desktop.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"module_name": "Community",
|
||||||
|
"color": "grey",
|
||||||
|
"icon": "octicon octicon-file-directory",
|
||||||
|
"type": "module",
|
||||||
|
"label": _("Community"),
|
||||||
|
}
|
||||||
|
]
|
||||||
12
lms/config/docs.py
Normal file
12
lms/config/docs.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
Configuration for docs
|
||||||
|
"""
|
||||||
|
|
||||||
|
# source_link = "https://github.com/[org_name]/community"
|
||||||
|
# docs_base_url = "https://[org_name].github.io/community"
|
||||||
|
# headline = "App that does everything"
|
||||||
|
# sub_heading = "Yes, you got that right the first time, everything"
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
context.brand_html = "Community"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"category": "Web Development",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2024-09-20 12:58:16.841571",
|
|
||||||
"name": "Web Development"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Business",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2024-09-20 12:58:32.304850",
|
|
||||||
"name": "Business"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Design",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2024-09-20 12:59:12.621022",
|
|
||||||
"name": "Design"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Personal Development",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2024-09-20 12:59:19.287404",
|
|
||||||
"name": "Personal Development"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Finance",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2024-09-20 12:58:28.579714",
|
|
||||||
"name": "Finance"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Frontend",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2024-05-08 14:05:16.979275",
|
|
||||||
"name": "Frontend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Framework",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "LMS Category",
|
|
||||||
"modified": "2023-06-15 18:01:41.598282",
|
|
||||||
"name": "Framework"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -115,7 +115,7 @@ scheduler_events = {
|
|||||||
"daily": ["lms.job.doctype.job_opportunity.job_opportunity.update_job_openings"],
|
"daily": ["lms.job.doctype.job_opportunity.job_opportunity.update_job_openings"],
|
||||||
}
|
}
|
||||||
|
|
||||||
fixtures = ["Custom Field", "Function", "Industry", "LMS Category"]
|
fixtures = ["Custom Field", "Function", "Industry"]
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
# -------
|
# -------
|
||||||
|
|||||||
156
lms/lms/api.py
156
lms/lms/api.py
@@ -7,7 +7,6 @@ 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()
|
||||||
@@ -289,13 +288,11 @@ def get_file_info(file_url):
|
|||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_branding():
|
def get_branding():
|
||||||
"""Get branding details."""
|
"""Get branding details."""
|
||||||
website_settings = frappe.get_single("Website Settings")
|
return {
|
||||||
image_fields = ["banner_image", "footer_logo", "favicon"]
|
"brand_name": frappe.db.get_single_value("Website Settings", "app_name"),
|
||||||
|
"brand_html": frappe.db.get_single_value("Website Settings", "brand_html"),
|
||||||
for field in image_fields:
|
"favicon": frappe.db.get_single_value("Website Settings", "favicon"),
|
||||||
website_settings.update({field: get_file_info(website_settings.get(field))})
|
}
|
||||||
|
|
||||||
return website_settings
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -583,7 +580,7 @@ def get_members(start=0, search=""):
|
|||||||
members = frappe.get_all(
|
members = frappe.get_all(
|
||||||
"User",
|
"User",
|
||||||
filters=filters,
|
filters=filters,
|
||||||
fields=["name", "full_name", "user_image", "username", "last_active"],
|
fields=["name", "full_name", "user_image", "username"],
|
||||||
page_length=20,
|
page_length=20,
|
||||||
start=start,
|
start=start,
|
||||||
)
|
)
|
||||||
@@ -613,144 +610,3 @@ def check_app_permission():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_payment_gateway_details(payment_gateway):
|
|
||||||
fields = []
|
|
||||||
gateway = frappe.get_doc("Payment Gateway", payment_gateway)
|
|
||||||
|
|
||||||
if gateway.gateway_controller is None:
|
|
||||||
try:
|
|
||||||
data = frappe.get_doc(f"{payment_gateway} Settings").as_dict()
|
|
||||||
meta = frappe.get_meta(f"{payment_gateway} Settings").fields
|
|
||||||
doctype = f"{payment_gateway} Settings"
|
|
||||||
docname = f"{payment_gateway} Settings"
|
|
||||||
except Exception:
|
|
||||||
frappe.throw(_("{0} Settings not found").format(payment_gateway))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
data = frappe.get_doc(gateway.gateway_settings, gateway.gateway_controller).as_dict()
|
|
||||||
meta = frappe.get_meta(gateway.gateway_settings).fields
|
|
||||||
doctype = gateway.gateway_settings
|
|
||||||
docname = gateway.gateway_controller
|
|
||||||
except Exception:
|
|
||||||
frappe.throw(_("{0} Settings not found").format(payment_gateway))
|
|
||||||
|
|
||||||
for row in meta:
|
|
||||||
if row.fieldtype not in ["Column Break", "Section Break"]:
|
|
||||||
if row.fieldtype in ["Attach", "Attach Image"]:
|
|
||||||
fieldtype = "Upload"
|
|
||||||
data[row.fieldname] = get_file_info(data.get(row.fieldname))
|
|
||||||
else:
|
|
||||||
fieldtype = row.fieldtype
|
|
||||||
|
|
||||||
fields.append(
|
|
||||||
{
|
|
||||||
"label": row.label,
|
|
||||||
"name": row.fieldname,
|
|
||||||
"type": fieldtype,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"fields": fields,
|
|
||||||
"data": data,
|
|
||||||
"doctype": doctype,
|
|
||||||
"docname": docname,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ from frappe.utils.telemetry import capture
|
|||||||
|
|
||||||
|
|
||||||
class CourseChapter(Document):
|
class CourseChapter(Document):
|
||||||
pass
|
def after_insert(self):
|
||||||
|
capture("chapter_created", "lms")
|
||||||
|
|||||||
@@ -1,4 +1,148 @@
|
|||||||
// Copyright (c) 2021, FOSS United and contributors
|
// Copyright (c) 2021, FOSS United and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Course Lesson", {});
|
frappe.ui.form.on("Course Lesson", {
|
||||||
|
setup: function (frm) {
|
||||||
|
frm.trigger("setup_help");
|
||||||
|
},
|
||||||
|
setup_help(frm) {
|
||||||
|
let quiz_link = `<a href="/app/lms-quiz"> ${__("Quiz List")} </a>`;
|
||||||
|
let exercise_link = `<a href="/app/lms-exercise"> ${__(
|
||||||
|
"Exercise List"
|
||||||
|
)} </a>`;
|
||||||
|
let file_link = `<a href="/app/file"> ${__("File DocType")} </a>`;
|
||||||
|
|
||||||
|
frm.get_field("help").html(`
|
||||||
|
<p>${__(
|
||||||
|
"You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same."
|
||||||
|
)}</p>
|
||||||
|
<table class="table">
|
||||||
|
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
|
||||||
|
<th style="width: 20%;">
|
||||||
|
${__("Content Type")}
|
||||||
|
</th>
|
||||||
|
<th style="width: 40%;">
|
||||||
|
${__("Syntax")}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
${__("Description")}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
${__("YouTube Video")}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ YouTubeVideo("unique_embed_id") }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
${__(
|
||||||
|
"Copy and paste the syntax in the editor. Replace 'embed_src' with the embed source that YouTube provides. To get the source, follow the steps mentioned below."
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ul class="p-4">
|
||||||
|
<li>
|
||||||
|
${__("Upload the video on youtube.")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
${__(
|
||||||
|
"When you share a youtube video, it shows an option called Embed."
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
${__(
|
||||||
|
"On clicking it, it provides an iframe. Copy the source (src) of the iframe and paste it here."
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
${__("Quiz")}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ Quiz("lms_quiz_id") }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${__(
|
||||||
|
"Copy and paste the syntax in the editor. Replace 'lms_quiz_id' with the ID of the Quiz you want to add. You can get the ID of the quiz from the {0}.",
|
||||||
|
[quiz_link]
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
${__("Video")}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ Video("url_of_source") }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${__(
|
||||||
|
"Upload a video from your local machine to the {0}. Copy and paste this syntax in the editor. Replace 'url_of_source' with the File URL field of the document you created in the File DocType.",
|
||||||
|
[file_link]
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
${"Exercise"}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ Exercise("exercise_id") }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${__(
|
||||||
|
"Copy and paste the syntax in the editor. Replace 'exercise_id' with the ID of the Exercise you want to add. You can get the ID of the exercise from the {0}.",
|
||||||
|
[exercise_link]
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
${__("Assignment")}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ Assignment("id-filetype") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<hr>
|
||||||
|
<table class="table">
|
||||||
|
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
|
||||||
|
<th style="width: 90%">
|
||||||
|
${__("Supported File Types for Assignment")}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
${__("Syntax")}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
.doc, .docx, .xml
|
||||||
|
<td>
|
||||||
|
${__("Document")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
.pdf
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${__("PDF")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
.png, .jpg, .jpeg
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${__("Image")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ 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)
|
||||||
@@ -113,8 +116,6 @@ 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
|
||||||
@@ -124,11 +125,6 @@ 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", properties={"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
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ import json
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, format_date, format_datetime, get_time, getdate, add_days
|
from frappe.utils import (
|
||||||
|
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,
|
||||||
get_lesson_url,
|
get_lesson_url,
|
||||||
get_quiz_details,
|
get_quiz_details,
|
||||||
get_assignment_details,
|
get_assignment_details,
|
||||||
update_payment_record,
|
|
||||||
)
|
)
|
||||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||||
|
|
||||||
@@ -27,7 +31,6 @@ class LMSBatch(Document):
|
|||||||
self.validate_batch_end_date()
|
self.validate_batch_end_date()
|
||||||
self.validate_duplicate_courses()
|
self.validate_duplicate_courses()
|
||||||
self.validate_duplicate_students()
|
self.validate_duplicate_students()
|
||||||
self.validate_payments_app()
|
|
||||||
self.validate_duplicate_assessments()
|
self.validate_duplicate_assessments()
|
||||||
self.validate_membership()
|
self.validate_membership()
|
||||||
self.validate_timetable()
|
self.validate_timetable()
|
||||||
@@ -57,12 +60,6 @@ class LMSBatch(Document):
|
|||||||
_("Course {0} has already been added to this batch.").format(frappe.bold(title))
|
_("Course {0} has already been added to this batch.").format(frappe.bold(title))
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_payments_app(self):
|
|
||||||
if self.paid_batch:
|
|
||||||
installed_apps = frappe.get_installed_apps()
|
|
||||||
if "payments" not in installed_apps:
|
|
||||||
frappe.throw(_("Please install the Payments app to create a paid batches."))
|
|
||||||
|
|
||||||
def validate_duplicate_assessments(self):
|
def validate_duplicate_assessments(self):
|
||||||
assessments = [row.assessment_name for row in self.assessment]
|
assessments = [row.assessment_name for row in self.assessment]
|
||||||
for assessment in self.assessment:
|
for assessment in self.assessment:
|
||||||
@@ -76,23 +73,21 @@ 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 (
|
if not student.confirmation_email_sent and (
|
||||||
not student.confirmation_email_sent
|
outgoing_email_account or frappe.conf.get("mail_login")
|
||||||
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"
|
||||||
@@ -172,9 +167,23 @@ class LMSBatch(Document):
|
|||||||
_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx)
|
_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_payment_authorized(self, payment_status):
|
|
||||||
if payment_status in ["Authorized", "Completed"]:
|
@frappe.whitelist()
|
||||||
update_payment_record("LMS Batch", self.name)
|
def remove_student(student, batch_name):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
|
frappe.db.delete("Batch Student", {"student": student, "parent": batch_name})
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_course(course, parent):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
|
frappe.db.delete("Batch Course", {"course": course, "parent": parent})
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_assessment(assessment, parent):
|
||||||
|
frappe.only_for("Moderator")
|
||||||
|
frappe.db.delete("LMS Assessment", {"assessment_name": assessment, "parent": parent})
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -15,13 +15,12 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Category",
|
"label": "Category",
|
||||||
"reqd": 1,
|
|
||||||
"unique": 1
|
"unique": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-09-23 19:33:49.593950",
|
"modified": "2023-06-15 15:14:11.341961",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Category",
|
"name": "LMS Category",
|
||||||
@@ -56,6 +55,5 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "category",
|
"title_field": "category"
|
||||||
"track_changes": 1
|
|
||||||
}
|
}
|
||||||
@@ -15,10 +15,8 @@
|
|||||||
"template",
|
"template",
|
||||||
"published",
|
"published",
|
||||||
"section_break_scyf",
|
"section_break_scyf",
|
||||||
"evaluator",
|
|
||||||
"evaluator_name",
|
|
||||||
"column_break_slaw",
|
|
||||||
"expiry_date",
|
"expiry_date",
|
||||||
|
"column_break_slaw",
|
||||||
"batch_name"
|
"batch_name"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -97,24 +95,11 @@
|
|||||||
{
|
{
|
||||||
"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-09-11 11:37:20.419955",
|
"modified": "2024-07-16 15:29:19.708888",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
|
|||||||
@@ -8,16 +8,12 @@
|
|||||||
"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",
|
||||||
@@ -107,33 +103,11 @@
|
|||||||
"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-09-11 11:20:06.233491",
|
"modified": "2024-07-16 14:06:11.977666",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Evaluation",
|
"name": "LMS Certificate Evaluation",
|
||||||
|
|||||||
@@ -7,17 +7,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"course",
|
"course",
|
||||||
"course_title",
|
"evaluator",
|
||||||
|
"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",
|
||||||
@@ -39,6 +33,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "member",
|
"fieldname": "member",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Member",
|
"label": "Member",
|
||||||
"options": "User",
|
"options": "User",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -46,9 +41,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",
|
||||||
@@ -108,47 +103,11 @@
|
|||||||
"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-09-11 11:19:44.669132",
|
"modified": "2024-04-16 11:01:28.336807",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from frappe.utils import (
|
|||||||
get_datetime,
|
get_datetime,
|
||||||
nowtime,
|
nowtime,
|
||||||
get_time,
|
get_time,
|
||||||
get_fullname,
|
|
||||||
)
|
)
|
||||||
from lms.lms.utils import get_evaluator
|
from lms.lms.utils import get_evaluator
|
||||||
import json
|
import json
|
||||||
@@ -33,30 +32,25 @@ class LMSCertificateRequest(Document):
|
|||||||
def set_evaluator(self):
|
def set_evaluator(self):
|
||||||
if not self.evaluator:
|
if not self.evaluator:
|
||||||
self.evaluator = get_evaluator(self.course, self.batch_name)
|
self.evaluator = get_evaluator(self.course, self.batch_name)
|
||||||
self.evaluator_name = get_fullname(self.evaluator)
|
|
||||||
|
|
||||||
def validate_unavailability(self):
|
def validate_unavailability(self):
|
||||||
if self.evaluator:
|
unavailable = frappe.db.get_value(
|
||||||
unavailable = frappe.db.get_value(
|
"Course Evaluator", self.evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
|
||||||
"Course Evaluator",
|
)
|
||||||
self.evaluator,
|
if (
|
||||||
["unavailable_from", "unavailable_to"],
|
unavailable.unavailable_from
|
||||||
as_dict=1,
|
and unavailable.unavailable_to
|
||||||
)
|
and getdate(self.date) >= unavailable.unavailable_from
|
||||||
if (
|
and getdate(self.date) <= unavailable.unavailable_to
|
||||||
unavailable.unavailable_from
|
):
|
||||||
and unavailable.unavailable_to
|
frappe.throw(
|
||||||
and getdate(self.date) >= unavailable.unavailable_from
|
_(
|
||||||
and getdate(self.date) <= unavailable.unavailable_to
|
"The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
|
||||||
):
|
).format(
|
||||||
frappe.throw(
|
format_date(unavailable.unavailable_from, "medium"),
|
||||||
_(
|
format_date(unavailable.unavailable_to, "medium"),
|
||||||
"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(
|
||||||
@@ -126,12 +120,14 @@ class LMSCertificateRequest(Document):
|
|||||||
template = "certificate_request_notification"
|
template = "certificate_request_notification"
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"course": self.course_title,
|
"course": frappe.db.get_value("LMS Course", self.course, "title"),
|
||||||
"timezone": self.timezone if self.batch_name else "",
|
"timezone": frappe.db.get_value("LMS Batch", self.batch_name, "timezone")
|
||||||
|
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": self.evaluator_name,
|
"evaluator": frappe.db.get_value("User", self.evaluator, "full_name"),
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
|
|||||||
@@ -16,12 +16,10 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"video_link",
|
"video_link",
|
||||||
|
"image",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"instructors",
|
"instructors",
|
||||||
"tags",
|
"tags",
|
||||||
"column_break_htgn",
|
|
||||||
"image",
|
|
||||||
"category",
|
|
||||||
"status",
|
"status",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
"published",
|
"published",
|
||||||
@@ -239,16 +237,6 @@
|
|||||||
"fieldname": "certification_tab",
|
"fieldname": "certification_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Certification"
|
"label": "Certification"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_htgn",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "category",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Category",
|
|
||||||
"options": "LMS Category"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -275,7 +263,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-09-21 10:23:58.633912",
|
"modified": "2024-07-12 13:54:40.474097",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import cint, today
|
from frappe.utils import cint, today
|
||||||
from frappe.utils.telemetry import capture
|
from frappe.utils.telemetry import capture
|
||||||
from lms.lms.utils import get_chapters, can_create_courses
|
from lms.lms.utils import get_chapters, can_create_courses
|
||||||
from ...utils import generate_slug, validate_image, update_payment_record
|
from ...utils import generate_slug, validate_image
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ class LMSCourse(Document):
|
|||||||
self.validate_instructors()
|
self.validate_instructors()
|
||||||
self.validate_video_link()
|
self.validate_video_link()
|
||||||
self.validate_status()
|
self.validate_status()
|
||||||
self.validate_payments_app()
|
|
||||||
self.image = validate_image(self.image)
|
self.image = validate_image(self.image)
|
||||||
|
|
||||||
def validate_published(self):
|
def validate_published(self):
|
||||||
@@ -45,19 +44,12 @@ class LMSCourse(Document):
|
|||||||
if self.published:
|
if self.published:
|
||||||
self.status = "Approved"
|
self.status = "Approved"
|
||||||
|
|
||||||
def validate_payments_app(self):
|
|
||||||
if self.paid_course:
|
|
||||||
installed_apps = frappe.get_installed_apps()
|
|
||||||
if "payments" not in installed_apps:
|
|
||||||
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||||
self.send_email_to_interested_users()
|
self.send_email_to_interested_users()
|
||||||
|
|
||||||
def on_payment_authorized(self, payment_status):
|
def after_insert(self):
|
||||||
if payment_status in ["Authorized", "Completed"]:
|
capture("course_created", "lms")
|
||||||
update_payment_record("LMS Course", self.name)
|
|
||||||
|
|
||||||
def send_email_to_interested_users(self):
|
def send_email_to_interested_users(self):
|
||||||
interested_users = frappe.get_all(
|
interested_users = frappe.get_all(
|
||||||
|
|||||||
@@ -2,28 +2,6 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("LMS Settings", {
|
frappe.ui.form.on("LMS Settings", {
|
||||||
setup: function (frm) {
|
// refresh: function(frm) {
|
||||||
frappe.call({
|
// }
|
||||||
method: "lms.lms.doctype.lms_settings.lms_settings.check_payments_app",
|
|
||||||
callback: (data) => {
|
|
||||||
if (!data.message) {
|
|
||||||
frm.set_df_property("payment_section", "hidden", 1);
|
|
||||||
frm.trigger("set_no_payments_app_html");
|
|
||||||
} else {
|
|
||||||
frm.set_df_property("no_payments_app", "hidden", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
set_no_payments_app_html(frm) {
|
|
||||||
frm.get_field("payments_app_is_not_installed").html(`
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
Please install the
|
|
||||||
<a target="_blank" style="color: var(--alert-text-warning); background: var(--alert-bg-warning);" href="https://frappecloud.com/marketplace/apps/payments">
|
|
||||||
Payments app
|
|
||||||
</a>
|
|
||||||
to enable payment gateway.
|
|
||||||
`);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,9 +23,15 @@
|
|||||||
"show_emails",
|
"show_emails",
|
||||||
"signup_settings_tab",
|
"signup_settings_tab",
|
||||||
"signup_settings_section",
|
"signup_settings_section",
|
||||||
"column_break_9",
|
"terms_of_use",
|
||||||
"custom_signup_content",
|
"terms_page",
|
||||||
"user_category",
|
"user_category",
|
||||||
|
"column_break_9",
|
||||||
|
"privacy_policy",
|
||||||
|
"privacy_policy_page",
|
||||||
|
"column_break_12",
|
||||||
|
"cookie_policy",
|
||||||
|
"cookie_policy_page",
|
||||||
"sidebar_tab",
|
"sidebar_tab",
|
||||||
"items_in_sidebar_section",
|
"items_in_sidebar_section",
|
||||||
"courses",
|
"courses",
|
||||||
@@ -42,15 +48,14 @@
|
|||||||
"mentor_request_status_update",
|
"mentor_request_status_update",
|
||||||
"payment_settings_tab",
|
"payment_settings_tab",
|
||||||
"payment_section",
|
"payment_section",
|
||||||
"payment_gateway",
|
"razorpay_key",
|
||||||
"default_currency",
|
"razorpay_secret",
|
||||||
"exception_country",
|
|
||||||
"column_break_cfcv",
|
|
||||||
"apply_gst",
|
"apply_gst",
|
||||||
|
"column_break_cfcv",
|
||||||
|
"default_currency",
|
||||||
"show_usd_equivalent",
|
"show_usd_equivalent",
|
||||||
"apply_rounding",
|
"apply_rounding",
|
||||||
"no_payments_app",
|
"exception_country",
|
||||||
"payments_app_is_not_installed",
|
|
||||||
"email_templates_tab",
|
"email_templates_tab",
|
||||||
"certification_template",
|
"certification_template",
|
||||||
"batch_confirmation_template",
|
"batch_confirmation_template",
|
||||||
@@ -87,14 +92,60 @@
|
|||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
"label": "Show Tab in Batch"
|
"label": "Show Tab in Batch"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "terms_of_use",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Terms of Use on Signup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "terms_of_use",
|
||||||
|
"fieldname": "terms_page",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Terms of Use Page",
|
||||||
|
"mandatory_depends_on": "terms_of_use",
|
||||||
|
"options": "Web Page"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "signup_settings_section",
|
"fieldname": "signup_settings_section",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "privacy_policy",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Privacy Policy on Signup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "privacy_policy",
|
||||||
|
"fieldname": "privacy_policy_page",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Privacy Policy Page",
|
||||||
|
"mandatory_depends_on": "privacy_policy",
|
||||||
|
"options": "Web Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_12",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_9",
|
"fieldname": "column_break_9",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "cookie_policy",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Cookie Policy on Signup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "cookie_policy",
|
||||||
|
"fieldname": "cookie_policy_page",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cookie Policy Page",
|
||||||
|
"mandatory_depends_on": "cookie_policy",
|
||||||
|
"options": "Web Page"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "user_category",
|
"fieldname": "user_category",
|
||||||
@@ -148,6 +199,16 @@
|
|||||||
"fieldname": "column_break_cfcv",
|
"fieldname": "column_break_cfcv",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "razorpay_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Razorpay Key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "razorpay_secret",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"label": "Razorpay Secret"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "apply_gst",
|
"fieldname": "apply_gst",
|
||||||
@@ -164,7 +225,7 @@
|
|||||||
"depends_on": "show_usd_equivalent",
|
"depends_on": "show_usd_equivalent",
|
||||||
"fieldname": "exception_country",
|
"fieldname": "exception_country",
|
||||||
"fieldtype": "Table MultiSelect",
|
"fieldtype": "Table MultiSelect",
|
||||||
"label": "Primary Countries",
|
"label": "Maintain Original Currency",
|
||||||
"options": "Payment Country"
|
"options": "Payment Country"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -317,31 +378,12 @@
|
|||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Sidebar Items",
|
"label": "Sidebar Items",
|
||||||
"options": "LMS Sidebar Item"
|
"options": "LMS Sidebar Item"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "custom_signup_content",
|
|
||||||
"fieldtype": "HTML Editor",
|
|
||||||
"label": "Custom Signup Content"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "payment_gateway",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Payment Gateway"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "no_payments_app",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "payments_app_is_not_installed",
|
|
||||||
"fieldtype": "HTML",
|
|
||||||
"label": "Payments app is not installed"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-10-01 12:15:49.800242",
|
"modified": "2024-08-13 19:02:58.714080",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
|
|||||||
@@ -39,32 +39,3 @@ class LMSSettings(Document):
|
|||||||
frappe.bold("Course Evaluator"),
|
frappe.bold("Course Evaluator"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def check_payments_app():
|
|
||||||
installed_apps = frappe.get_installed_apps()
|
|
||||||
if "payments" not in installed_apps:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
filters = {
|
|
||||||
"doctype_or_field": "DocField",
|
|
||||||
"doc_type": "LMS Settings",
|
|
||||||
"field_name": "payment_gateway",
|
|
||||||
}
|
|
||||||
if frappe.db.exists("Property Setter", filters):
|
|
||||||
return True
|
|
||||||
|
|
||||||
link_property = frappe.new_doc("Property Setter")
|
|
||||||
link_property.update(filters)
|
|
||||||
link_property.property = "fieldtype"
|
|
||||||
link_property.value = "Link"
|
|
||||||
link_property.save()
|
|
||||||
|
|
||||||
options_property = frappe.new_doc("Property Setter")
|
|
||||||
options_property.update(filters)
|
|
||||||
options_property.property = "options"
|
|
||||||
options_property.value = "Payment Gateway"
|
|
||||||
options_property.save()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{% 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>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
# do your magic here
|
||||||
|
pass
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<p> {{ _("Hey {0}").format(doc.member_name) }} </p>
|
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
|
||||||
<br>
|
{% set timezone = frappe.db.get_value("LMS Batch", doc.batch, "timezone") %}
|
||||||
<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 timezone = timezone if timezone else '' %}
|
||||||
<br>
|
{% set evaluator_name = frappe.db.get_value("User", doc.evaluator, "full_name") %}
|
||||||
<p> {{ _("{0} is your evaluator").format(doc.evaluator_name) }} </p>
|
|
||||||
<br>
|
<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> {{ _("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": "<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": "{% 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_type": "HTML",
|
"message_type": "HTML",
|
||||||
"modified": "2024-09-05 16:33:42.212842",
|
"modified": "2024-07-10 15:51:33.803704",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@erpnext.com",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Certificate Request Reminder",
|
"name": "Certificate Request Reminder",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from payments.utils import get_payment_gateway_controller
|
|
||||||
|
|
||||||
|
|
||||||
def get_payment_gateway():
|
|
||||||
return frappe.db.get_single_value("LMS Settings", "payment_gateway")
|
|
||||||
|
|
||||||
|
|
||||||
def get_controller(payment_gateway):
|
|
||||||
return get_payment_gateway_controller(payment_gateway)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_currency(payment_gateway, currency):
|
|
||||||
controller = get_controller(payment_gateway)
|
|
||||||
controller().validate_transaction_currency(currency)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_payment_link(doctype, docname, title, amount, total_amount, currency, address):
|
|
||||||
payment_gateway = get_payment_gateway()
|
|
||||||
address = frappe._dict(address)
|
|
||||||
amount_with_gst = total_amount if total_amount != amount else 0
|
|
||||||
|
|
||||||
payment = record_payment(address, doctype, docname, amount, currency, amount_with_gst)
|
|
||||||
controller = get_controller(payment_gateway)
|
|
||||||
|
|
||||||
if doctype == "LMS Course":
|
|
||||||
redirect_to = f"/lms/courses/{docname}/learn/1-1"
|
|
||||||
elif doctype == "LMS Batch":
|
|
||||||
redirect_to = f"/lms/batches/{docname}"
|
|
||||||
|
|
||||||
payment_details = {
|
|
||||||
"amount": total_amount,
|
|
||||||
"title": f"Payment for {doctype} {title} {docname}",
|
|
||||||
"description": f"{address.billing_name}'s payment for {title}",
|
|
||||||
"reference_doctype": doctype,
|
|
||||||
"reference_docname": docname,
|
|
||||||
"payer_email": frappe.session.user,
|
|
||||||
"payer_name": address.billing_name,
|
|
||||||
"currency": currency,
|
|
||||||
"payment_gateway": payment_gateway,
|
|
||||||
"redirect_to": redirect_to,
|
|
||||||
"payment": payment.name,
|
|
||||||
}
|
|
||||||
url = controller.get_payment_url(**payment_details)
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
def record_payment(address, doctype, docname, amount, currency, amount_with_gst=0):
|
|
||||||
address = frappe._dict(address)
|
|
||||||
address_name = save_address(address)
|
|
||||||
|
|
||||||
payment_doc = frappe.new_doc("LMS Payment")
|
|
||||||
payment_doc.update(
|
|
||||||
{
|
|
||||||
"member": frappe.session.user,
|
|
||||||
"billing_name": address.billing_name,
|
|
||||||
"address": address_name,
|
|
||||||
"amount": amount,
|
|
||||||
"currency": currency,
|
|
||||||
"amount_with_gst": amount_with_gst,
|
|
||||||
"gstin": address.gstin,
|
|
||||||
"pan": address.pan,
|
|
||||||
"source": address.source,
|
|
||||||
"payment_for_document_type": doctype,
|
|
||||||
"payment_for_document": docname,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
payment_doc.save(ignore_permissions=True)
|
|
||||||
return payment_doc
|
|
||||||
|
|
||||||
|
|
||||||
def save_address(address):
|
|
||||||
filters = {"email_id": frappe.session.user}
|
|
||||||
exists = frappe.db.exists("Address", filters)
|
|
||||||
if exists:
|
|
||||||
address_doc = frappe.get_last_doc("Address", filters=filters)
|
|
||||||
else:
|
|
||||||
address_doc = frappe.new_doc("Address")
|
|
||||||
|
|
||||||
address_doc.update(address)
|
|
||||||
address_doc.update(
|
|
||||||
{
|
|
||||||
"address_title": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
|
||||||
"address_type": "Billing",
|
|
||||||
"is_primary_address": 1,
|
|
||||||
"email_id": frappe.session.user,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
address_doc.save(ignore_permissions=True)
|
|
||||||
return address_doc.name
|
|
||||||
291
lms/lms/utils.py
291
lms/lms/utils.py
@@ -722,6 +722,17 @@ def get_lesson_count(course):
|
|||||||
return lesson_count
|
return lesson_count
|
||||||
|
|
||||||
|
|
||||||
|
def get_restriction_details():
|
||||||
|
user = frappe.db.get_value(
|
||||||
|
"User", frappe.session.user, ["profile_complete", "username"], as_dict=True
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"restrict": not user.profile_complete,
|
||||||
|
"username": user.username,
|
||||||
|
"prefix": frappe.get_hooks("profile_url_prefix")[0] or "/users/",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_all_memberships(member):
|
def get_all_memberships(member):
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
"LMS Enrollment",
|
"LMS Enrollment",
|
||||||
@@ -908,6 +919,39 @@ def get_upcoming_evals(student, courses):
|
|||||||
return upcoming_evals
|
return upcoming_evals
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_payment_options(doctype, docname, phone, country):
|
||||||
|
if not frappe.db.exists(doctype, docname):
|
||||||
|
frappe.throw(_("Invalid document provided."))
|
||||||
|
|
||||||
|
validate_phone_number(phone, True)
|
||||||
|
details = get_details(doctype, docname)
|
||||||
|
|
||||||
|
details.amount, details.currency = check_multicurrency(
|
||||||
|
details.amount, details.currency, country, details.amount_usd
|
||||||
|
)
|
||||||
|
if details.currency == "INR":
|
||||||
|
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
||||||
|
|
||||||
|
client = get_client()
|
||||||
|
order = create_order(client, details.amount, details.currency)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
"key_id": frappe.db.get_single_value("LMS Settings", "razorpay_key"),
|
||||||
|
"name": frappe.db.get_single_value("Website Settings", "app_name"),
|
||||||
|
"description": _("Payment for {0} course").format(details["title"]),
|
||||||
|
"order_id": order["id"],
|
||||||
|
"amount": cint(order["amount"]) * 100,
|
||||||
|
"currency": order["currency"],
|
||||||
|
"prefill": {
|
||||||
|
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||||
|
"email": frappe.session.user,
|
||||||
|
"contact": phone,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
def check_multicurrency(amount, currency, country=None, amount_usd=None):
|
def check_multicurrency(amount, currency, country=None, amount_usd=None):
|
||||||
settings = frappe.get_single("LMS Settings")
|
settings = frappe.get_single("LMS Settings")
|
||||||
show_usd_equivalent = settings.show_usd_equivalent
|
show_usd_equivalent = settings.show_usd_equivalent
|
||||||
@@ -965,6 +1009,145 @@ def apply_gst(amount, country=None):
|
|||||||
return amount, gst_applied
|
return amount, gst_applied
|
||||||
|
|
||||||
|
|
||||||
|
def get_details(doctype, docname):
|
||||||
|
if doctype == "LMS Course":
|
||||||
|
details = frappe.db.get_value(
|
||||||
|
"LMS Course",
|
||||||
|
docname,
|
||||||
|
["name", "title", "paid_course", "currency", "course_price as amount", "amount_usd"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
if not details.paid_course:
|
||||||
|
frappe.throw(_("This course is free."))
|
||||||
|
else:
|
||||||
|
details = frappe.db.get_value(
|
||||||
|
"LMS Batch",
|
||||||
|
docname,
|
||||||
|
["name", "title", "paid_batch", "currency", "amount", "amount_usd"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
if not details.paid_batch:
|
||||||
|
frappe.throw(_("To join this batch, please contact the Administrator."))
|
||||||
|
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
def save_address(address):
|
||||||
|
filters = {"email_id": frappe.session.user}
|
||||||
|
exists = frappe.db.exists("Address", filters)
|
||||||
|
if exists:
|
||||||
|
address_doc = frappe.get_last_doc("Address", filters=filters)
|
||||||
|
else:
|
||||||
|
address_doc = frappe.new_doc("Address")
|
||||||
|
|
||||||
|
address_doc.update(address)
|
||||||
|
address_doc.update(
|
||||||
|
{
|
||||||
|
"address_title": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||||
|
"address_type": "Billing",
|
||||||
|
"is_primary_address": 1,
|
||||||
|
"email_id": frappe.session.user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
address_doc.save(ignore_permissions=True)
|
||||||
|
return address_doc.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
settings = frappe.get_single("LMS Settings")
|
||||||
|
razorpay_key = settings.razorpay_key
|
||||||
|
razorpay_secret = settings.get_password("razorpay_secret", raise_exception=True)
|
||||||
|
|
||||||
|
if not razorpay_key and not razorpay_secret:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return razorpay.Client(auth=(razorpay_key, razorpay_secret))
|
||||||
|
|
||||||
|
|
||||||
|
def create_order(client, amount, currency):
|
||||||
|
try:
|
||||||
|
return client.order.create(
|
||||||
|
{
|
||||||
|
"amount": cint(amount) * 100,
|
||||||
|
"currency": currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
|
||||||
|
).format(e, amount, currency, cint(amount))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def verify_payment(response, doctype, docname, address, order_id):
|
||||||
|
client = get_client()
|
||||||
|
client.utility.verify_payment_signature(
|
||||||
|
{
|
||||||
|
"razorpay_order_id": order_id,
|
||||||
|
"razorpay_payment_id": response["razorpay_payment_id"],
|
||||||
|
"razorpay_signature": response["razorpay_signature"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payment = record_payment(address, response, client, doctype, docname)
|
||||||
|
if doctype == "LMS Course":
|
||||||
|
return create_membership(docname, payment)
|
||||||
|
else:
|
||||||
|
return add_student_to_batch(docname, payment)
|
||||||
|
|
||||||
|
|
||||||
|
def record_payment(address, response, client, doctype, docname):
|
||||||
|
address = frappe._dict(address)
|
||||||
|
address_name = save_address(address)
|
||||||
|
|
||||||
|
payment_details = get_payment_details(doctype, docname, address)
|
||||||
|
payment_doc = frappe.new_doc("LMS Payment")
|
||||||
|
payment_doc.update(
|
||||||
|
{
|
||||||
|
"member": frappe.session.user,
|
||||||
|
"billing_name": address.billing_name,
|
||||||
|
"address": address_name,
|
||||||
|
"payment_received": 1,
|
||||||
|
"order_id": response["razorpay_order_id"],
|
||||||
|
"payment_id": response["razorpay_payment_id"],
|
||||||
|
"amount": payment_details["amount"],
|
||||||
|
"currency": payment_details["currency"],
|
||||||
|
"amount_with_gst": payment_details["amount_with_gst"],
|
||||||
|
"gstin": address.gstin,
|
||||||
|
"pan": address.pan,
|
||||||
|
"source": address.source,
|
||||||
|
"payment_for_document_type": doctype,
|
||||||
|
"payment_for_document": docname,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
payment_doc.save(ignore_permissions=True)
|
||||||
|
return payment_doc
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_details(doctype, docname, address):
|
||||||
|
amount_field = "course_price" if doctype == "LMS Course" else "amount"
|
||||||
|
amount = frappe.db.get_value(doctype, docname, amount_field)
|
||||||
|
currency = frappe.db.get_value(doctype, docname, "currency")
|
||||||
|
amount_usd = frappe.db.get_value(doctype, docname, "amount_usd")
|
||||||
|
amount_with_gst = 0
|
||||||
|
|
||||||
|
amount, currency = check_multicurrency(amount, currency, None, amount_usd)
|
||||||
|
if currency == "INR" and address.country == "India":
|
||||||
|
amount_with_gst, gst_applied = apply_gst(amount, address.country)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"amount": amount,
|
||||||
|
"currency": currency,
|
||||||
|
"amount_with_gst": amount_with_gst,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def create_membership(course, payment):
|
def create_membership(course, payment):
|
||||||
membership = frappe.new_doc("LMS Enrollment")
|
membership = frappe.new_doc("LMS Enrollment")
|
||||||
membership.update(
|
membership.update(
|
||||||
@@ -974,6 +1157,24 @@ def create_membership(course, payment):
|
|||||||
return f"/lms/courses/{course}/learn/1-1"
|
return f"/lms/courses/{course}/learn/1-1"
|
||||||
|
|
||||||
|
|
||||||
|
def add_student_to_batch(batchname, payment):
|
||||||
|
student = frappe.new_doc("Batch Student")
|
||||||
|
current_count = frappe.db.count("Batch Student", {"parent": batchname})
|
||||||
|
student.update(
|
||||||
|
{
|
||||||
|
"student": frappe.session.user,
|
||||||
|
"payment": payment.name,
|
||||||
|
"source": payment.source,
|
||||||
|
"parent": batchname,
|
||||||
|
"parenttype": "LMS Batch",
|
||||||
|
"parentfield": "students",
|
||||||
|
"idx": current_count + 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
student.save(ignore_permissions=True)
|
||||||
|
return f"/batches/{batchname}"
|
||||||
|
|
||||||
|
|
||||||
def get_current_exchange_rate(source, target="USD"):
|
def get_current_exchange_rate(source, target="USD"):
|
||||||
url = f"https://api.frankfurter.app/latest?from={source}&to={target}"
|
url = f"https://api.frankfurter.app/latest?from={source}&to={target}"
|
||||||
|
|
||||||
@@ -1019,7 +1220,6 @@ def get_course_details(course):
|
|||||||
"featured",
|
"featured",
|
||||||
"disable_self_learning",
|
"disable_self_learning",
|
||||||
"published_on",
|
"published_on",
|
||||||
"category",
|
|
||||||
"status",
|
"status",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
"course_price",
|
"course_price",
|
||||||
@@ -1575,11 +1775,10 @@ def get_order_summary(doctype, docname, country=None):
|
|||||||
details.amount, details.currency = check_multicurrency(
|
details.amount, details.currency = check_multicurrency(
|
||||||
details.amount, details.currency, country, details.amount_usd
|
details.amount, details.currency, country, details.amount_usd
|
||||||
)
|
)
|
||||||
details.original_amount = details.amount
|
|
||||||
details.original_amount_formatted = fmt_money(details.amount, 0, details.currency)
|
details.original_amount_formatted = fmt_money(details.amount, 0, details.currency)
|
||||||
|
|
||||||
if details.currency == "INR":
|
if details.currency == "INR":
|
||||||
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
details.amount, details.gst_applied = apply_gst(details.amount)
|
||||||
details.gst_amount_formatted = fmt_money(details.gst_applied, 0, details.currency)
|
details.gst_amount_formatted = fmt_money(details.gst_applied, 0, details.currency)
|
||||||
|
|
||||||
details.total_amount_formatted = fmt_money(details.amount, 0, details.currency)
|
details.total_amount_formatted = fmt_money(details.amount, 0, details.currency)
|
||||||
@@ -1637,89 +1836,3 @@ def publish_notifications(doc, method):
|
|||||||
frappe.publish_realtime(
|
frappe.publish_realtime(
|
||||||
"publish_lms_notifications", user=doc.for_user, after_commit=True
|
"publish_lms_notifications", user=doc.for_user, after_commit=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_payment_record(doctype, docname):
|
|
||||||
request = frappe.get_all(
|
|
||||||
"Integration Request",
|
|
||||||
{
|
|
||||||
"reference_doctype": doctype,
|
|
||||||
"reference_docname": docname,
|
|
||||||
"owner": frappe.session.user,
|
|
||||||
},
|
|
||||||
order_by="creation desc",
|
|
||||||
limit=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(request):
|
|
||||||
data = frappe.db.get_value("Integration Request", request[0].name, "data")
|
|
||||||
data = frappe._dict(json.loads(data))
|
|
||||||
|
|
||||||
payment_gateway = data.get("payment_gateway")
|
|
||||||
if payment_gateway == "Razorpay":
|
|
||||||
payment_id = "razorpay_payment_id"
|
|
||||||
elif "Stripe" in payment_gateway:
|
|
||||||
payment_id = "stripe_token_id"
|
|
||||||
else:
|
|
||||||
payment_id = "order_id"
|
|
||||||
|
|
||||||
frappe.db.set_value(
|
|
||||||
"LMS Payment",
|
|
||||||
data.payment,
|
|
||||||
{
|
|
||||||
"payment_received": 1,
|
|
||||||
"payment_id": data.get(payment_id),
|
|
||||||
"order_id": data.get("order_id"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if doctype == "LMS Course":
|
|
||||||
enroll_in_course(data.payment, docname)
|
|
||||||
else:
|
|
||||||
enroll_in_batch(data.payment, docname)
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error(frappe.get_traceback(), _("Enrollment Failed"))
|
|
||||||
|
|
||||||
|
|
||||||
def enroll_in_course(payment_name, course):
|
|
||||||
if not frappe.db.exists(
|
|
||||||
"LMS Enrollment", {"member": frappe.session.user, "course": course}
|
|
||||||
):
|
|
||||||
enrollment = frappe.new_doc("LMS Enrollment")
|
|
||||||
payment = frappe.db.get_value(
|
|
||||||
"LMS Payment", payment_name, ["name", "source"], as_dict=True
|
|
||||||
)
|
|
||||||
|
|
||||||
enrollment.update(
|
|
||||||
{
|
|
||||||
"member": frappe.session.user,
|
|
||||||
"course": course,
|
|
||||||
"payment": payment.name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
enrollment.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
|
|
||||||
def enroll_in_batch(payment_name, batch):
|
|
||||||
if not frappe.db.exists(
|
|
||||||
"Batch Student", {"parent": batch, "student": frappe.session.user}
|
|
||||||
):
|
|
||||||
student = frappe.new_doc("Batch Student")
|
|
||||||
current_count = frappe.db.count("Batch Student", {"parent": batch})
|
|
||||||
payment = frappe.db.get_value(
|
|
||||||
"LMS Payment", payment_name, ["name", "source"], as_dict=True
|
|
||||||
)
|
|
||||||
|
|
||||||
student.update(
|
|
||||||
{
|
|
||||||
"student": frappe.session.user,
|
|
||||||
"payment": payment.name,
|
|
||||||
"source": payment.source,
|
|
||||||
"parent": batch,
|
|
||||||
"parenttype": "LMS Batch",
|
|
||||||
"parentfield": "students",
|
|
||||||
"idx": current_count + 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
student.save(ignore_permissions=True)
|
|
||||||
|
|||||||
3330
lms/locale/main.pot
3330
lms/locale/main.pot
File diff suppressed because it is too large
Load Diff
@@ -90,4 +90,3 @@ 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 #18-09-2024
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
delete_notification("Certificate Request Creation")
|
|
||||||
delete_notification("Certificate Request Reminder")
|
|
||||||
|
|
||||||
|
|
||||||
def delete_notification(notification_name):
|
|
||||||
if frappe.db.exists("Notification", notification_name):
|
|
||||||
frappe.db.delete("Notification", notification_name)
|
|
||||||
@@ -227,7 +227,8 @@ def assignment_renderer(detail):
|
|||||||
|
|
||||||
|
|
||||||
def show_custom_signup():
|
def show_custom_signup():
|
||||||
settings = frappe.get_single("LMS Settings")
|
if frappe.db.get_single_value(
|
||||||
if settings.custom_signup_content or settings.user_category:
|
"LMS Settings", "terms_of_use"
|
||||||
|
) or frappe.db.get_single_value("LMS Settings", "privacy_policy"):
|
||||||
return "lms/templates/signup-form.html"
|
return "lms/templates/signup-form.html"
|
||||||
return "frappe/templates/signup.html"
|
return "frappe/templates/signup.html"
|
||||||
|
|||||||
@@ -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: pointer;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-header {
|
.schedule-header {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
<p> {{ _("Hey {0}").format(member_name) }} </p>
|
<p> {{ _("Hey {0}").format(member_name) }} </p>
|
||||||
<br>
|
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, date, start_time, timezone) }}</p>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{% set custom_signup_content = frappe.db.get_single_value("LMS Settings", "custom_signup_content") %}
|
|
||||||
<form class="signup-form" role="form">
|
<form class="signup-form" role="form">
|
||||||
<div class="page-card-body">
|
<div class="page-card-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -32,7 +31,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if custom_signup_content %}
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
@@ -41,12 +39,11 @@
|
|||||||
data-fieldtype="Check" data-fieldname="terms" id="signup-terms" required>
|
data-fieldtype="Check" data-fieldname="terms" id="signup-terms" required>
|
||||||
</span>
|
</span>
|
||||||
<span class="label-area">
|
<span class="label-area">
|
||||||
{{ custom_signup_content }}
|
{{ _("I have read and agree to your {0}").format(get_signup_optin_checks()) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="page-card-actions">
|
<div class="page-card-actions">
|
||||||
<button class="btn btn-sm btn-primary btn-block btn-signup"
|
<button class="btn btn-sm btn-primary btn-block btn-signup"
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ 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
|
||||||
capture("active_site", "lms")
|
if frappe.session.user != "Guest":
|
||||||
|
capture("active_site", "lms")
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@@ -148,9 +149,8 @@ def get_meta(app_path):
|
|||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if user.bio:
|
soup = BeautifulSoup(user.bio, "html.parser")
|
||||||
soup = BeautifulSoup(user.bio, "html.parser")
|
user.bio = soup.get_text()
|
||||||
user.bio = soup.get_text()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"title": user.full_name,
|
"title": user.full_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user