mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge pull request #1942 from frappe/develop
chore: merge 'develop' into 'main'
This commit is contained in:
@@ -55,8 +55,9 @@
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"autoprefixer": "10.4.2",
|
||||
"postcss": "8.4.5",
|
||||
"vite": "5.0.11",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"vite": "5.0.11",
|
||||
"vite-plugin-pwa": "0.15.0"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -26,28 +26,51 @@
|
||||
v-model="quiz"
|
||||
doctype="LMS Quiz"
|
||||
:label="__('Select a quiz')"
|
||||
placeholder=" "
|
||||
:onCreate="(value, close) => redirectToForm()"
|
||||
/>
|
||||
<Link
|
||||
v-else
|
||||
v-model="assignment"
|
||||
doctype="LMS Assignment"
|
||||
:label="__('Select an assignment')"
|
||||
:onCreate="(value, close) => redirectToForm()"
|
||||
/>
|
||||
<div v-else class="space-y-4">
|
||||
<Link
|
||||
v-if="filterAssignmentsByCourse"
|
||||
v-model="assignment"
|
||||
doctype="LMS Assignment"
|
||||
:filters="{
|
||||
course: route.params.courseName,
|
||||
}"
|
||||
placeholder=" "
|
||||
:label="__('Select an Assignment')"
|
||||
:onCreate="(value, close) => redirectToForm()"
|
||||
/>
|
||||
<Link
|
||||
v-else
|
||||
v-model="assignment"
|
||||
doctype="LMS Assignment"
|
||||
placeholder=" "
|
||||
:label="__('Select an Assignment')"
|
||||
:onCreate="(value, close) => redirectToForm()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
:label="__('Filter assignments by course')"
|
||||
v-model="filterAssignmentsByCourse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog } from 'frappe-ui'
|
||||
import { onMounted, ref, nextTick } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { Dialog, FormControl } from 'frappe-ui'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Link } from 'frappe-ui/frappe'
|
||||
|
||||
const show = ref(false)
|
||||
const quiz = ref(null)
|
||||
const assignment = ref(null)
|
||||
const filterAssignmentsByCourse = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="user.data?.is_student">
|
||||
<div>
|
||||
<div class="leading-5 mb-4">
|
||||
<div class="leading-5 mb-4 text-ink-gray-7">
|
||||
<div v-if="readOnly">
|
||||
{{ __('Thank you for providing your feedback.') }}
|
||||
<span
|
||||
|
||||
@@ -68,11 +68,12 @@ const props = defineProps({
|
||||
|
||||
const certification = createResource({
|
||||
url: 'lms.lms.api.get_certification_details',
|
||||
params: {
|
||||
course: props.courseName,
|
||||
makeParams(values) {
|
||||
return {
|
||||
course: props.courseName,
|
||||
}
|
||||
},
|
||||
auto: user.data ? true : false,
|
||||
cache: ['certificationData', user.data?.name],
|
||||
})
|
||||
|
||||
const downloadCertificate = () => {
|
||||
|
||||
@@ -220,8 +220,12 @@ function enrollStudent() {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
}, 500)
|
||||
} else {
|
||||
call('lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership', {
|
||||
course: props.course.data.name,
|
||||
call('frappe.client.insert', {
|
||||
doc: {
|
||||
doctype: 'LMS Enrollment',
|
||||
course: props.course.data.name,
|
||||
member: user.data.name,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
capture('enrolled_in_course', {
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
:label="__('Submission Type')"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
v-model="assignment.course"
|
||||
:label="__('Course')"
|
||||
doctype="LMS Course"
|
||||
placeholder=" "
|
||||
/>
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-2">
|
||||
{{ __('Question') }}
|
||||
@@ -67,6 +73,7 @@
|
||||
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { escapeHTML, sanitizeHTML } from '@/utils'
|
||||
import { Link } from 'frappe-ui/frappe'
|
||||
|
||||
const show = defineModel()
|
||||
const assignments = defineModel<Assignments>('assignments')
|
||||
@@ -75,6 +82,7 @@ interface Assignment {
|
||||
title: string
|
||||
type: string
|
||||
question: string
|
||||
course?: string
|
||||
}
|
||||
|
||||
interface Assignments {
|
||||
@@ -89,6 +97,7 @@ const assignment = reactive({
|
||||
title: '',
|
||||
type: '',
|
||||
question: '',
|
||||
course: '',
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -107,6 +116,7 @@ watch(
|
||||
assignment.title = row.title
|
||||
assignment.type = row.type
|
||||
assignment.question = row.question
|
||||
assignment.course = row.course || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,23 +3,11 @@
|
||||
:options="{
|
||||
title: 'Edit your profile',
|
||||
size: '3xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Save',
|
||||
variant: 'solid',
|
||||
onClick: (close) => saveProfile(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<div class="space-y-4">
|
||||
<!-- <Uploader
|
||||
v-model="profile.image.file_url"
|
||||
label="Profile Image"
|
||||
description="Your profile image to help others recognize you."
|
||||
/> -->
|
||||
<div>
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1">
|
||||
{{ __('Profile Image') }}
|
||||
@@ -47,16 +35,16 @@
|
||||
<div v-else class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="profile.image.file_url"
|
||||
:src="profile.image?.file_url"
|
||||
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
|
||||
/>
|
||||
|
||||
<div class="text-base flex flex-col ml-2">
|
||||
<span>
|
||||
{{ profile.image.file_name }}
|
||||
{{ profile.image?.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-ink-gray-4 mt-1">
|
||||
{{ getFileSize(profile.image.file_size) }}
|
||||
{{ getFileSize(profile.image?.file_size) }}
|
||||
</span>
|
||||
</div>
|
||||
<X
|
||||
@@ -66,39 +54,79 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl v-model="profile.first_name" :label="__('First Name')" />
|
||||
<FormControl v-model="profile.last_name" :label="__('Last Name')" />
|
||||
<FormControl v-model="profile.headline" :label="__('Headline')" />
|
||||
<Link
|
||||
:label="__('Language')"
|
||||
v-model="profile.language"
|
||||
doctype="Language"
|
||||
<Switch
|
||||
v-model="profile.looking_for_job"
|
||||
:label="__('Open to Opportunities')"
|
||||
:description="
|
||||
__('Show recruiters and others that you are open to work.')
|
||||
"
|
||||
class="!px-0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Bio') }}
|
||||
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<FormControl
|
||||
v-model="profile.first_name"
|
||||
:label="__('First Name')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="profile.last_name"
|
||||
:label="__('Last Name')"
|
||||
/>
|
||||
<FormControl v-model="profile.headline" :label="__('Headline')" />
|
||||
|
||||
<FormControl
|
||||
v-model="profile.linkedin"
|
||||
:label="__('LinkedIn ID')"
|
||||
/>
|
||||
<FormControl v-model="profile.github" :label="__('GitHub ID')" />
|
||||
<FormControl
|
||||
v-model="profile.twitter"
|
||||
:label="__('Twitter ID')"
|
||||
/>
|
||||
</div>
|
||||
<TextEditor
|
||||
:fixedMenu="true"
|
||||
@change="(val) => (profile.bio = val)"
|
||||
:content="profile.bio"
|
||||
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<Link
|
||||
:label="__('Language')"
|
||||
v-model="profile.language"
|
||||
doctype="Language"
|
||||
/>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Bio') }}
|
||||
</div>
|
||||
<TextEditor
|
||||
:fixedMenu="true"
|
||||
@change="(val) => (profile.bio = val)"
|
||||
:content="profile.bio"
|
||||
:rows="15"
|
||||
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="pb-5 float-right">
|
||||
<Button variant="solid" @click="saveProfile(close)">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Button,
|
||||
createResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
FileUploader,
|
||||
Button,
|
||||
createResource,
|
||||
Switch,
|
||||
TextEditor,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
@@ -123,6 +151,10 @@ const profile = reactive({
|
||||
headline: '',
|
||||
bio: '',
|
||||
image: '',
|
||||
looking_for_job: false,
|
||||
linkedin: '',
|
||||
github: '',
|
||||
twitter: '',
|
||||
})
|
||||
|
||||
const imageResource = createResource({
|
||||
@@ -145,7 +177,7 @@ const updateProfile = createResource({
|
||||
doctype: 'User',
|
||||
name: props.profile.data.name,
|
||||
fieldname: {
|
||||
user_image: profile.image.file_url,
|
||||
user_image: profile.image?.file_url || null,
|
||||
...profile,
|
||||
},
|
||||
}
|
||||
@@ -199,6 +231,10 @@ watch(
|
||||
profile.headline = newVal.headline
|
||||
profile.language = newVal.language
|
||||
profile.bio = newVal.bio
|
||||
profile.looking_for_job = newVal.looking_for_job
|
||||
profile.linkedin = newVal.linkedin
|
||||
profile.github = newVal.github
|
||||
profile.twitter = newVal.twitter
|
||||
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Schedule Evaluation'),
|
||||
title: __('Schedule your evaluation'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
@@ -14,52 +14,49 @@
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Course') }}
|
||||
<div class="flex flex-col gap-4 text-base max-h-[60vh]">
|
||||
<FormControl
|
||||
v-model="evaluation.course"
|
||||
type="select"
|
||||
:label="__('Course')"
|
||||
:options="getCourses()"
|
||||
/>
|
||||
<div v-if="slots.data?.length" class="space-y-4 overflow-y-auto mt-4">
|
||||
<div class="text-ink-gray-9 font-medium">
|
||||
{{ __('Available Slots') }}
|
||||
</div>
|
||||
<Select v-model="evaluation.course" :options="getCourses()" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Date') }}
|
||||
</div>
|
||||
<FormControl
|
||||
type="date"
|
||||
v-model="evaluation.date"
|
||||
:min="
|
||||
dayjs()
|
||||
.add(dayjs.duration({ days: 1 }))
|
||||
.format('YYYY-MM-DD')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="slots.data?.length">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
{{ __('Select a slot') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-for="slot in slots.data">
|
||||
<div
|
||||
class="text-base text-center border rounded-md text-ink-gray-8 bg-surface-gray-3 p-2 cursor-pointer"
|
||||
@click="saveSlot(slot)"
|
||||
:class="{
|
||||
'border-outline-gray-4':
|
||||
evaluation.start_time == slot.start_time,
|
||||
}"
|
||||
>
|
||||
{{ formatTime(slot.start_time) }} -
|
||||
{{ formatTime(slot.end_time) }}
|
||||
<div class="space-y-5">
|
||||
<div v-for="row in slots.data" class="space-y-2">
|
||||
<div class="flex items-center text-ink-gray-7 space-x-2">
|
||||
<Calendar class="size-3" />
|
||||
<div class="text-ink-gray-9">
|
||||
{{ dayjs(row.date).format('DD MMMM YYYY') }}
|
||||
</div>
|
||||
<div>·</div>
|
||||
<div class="text-ink-gray-5">
|
||||
{{ row.day }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
v-for="slot in row.slots"
|
||||
class="text-base text-center border rounded-md text-ink-gray-8 p-2 cursor-pointer text-ink-gray-7 hover:bg-surface-gray-2 hover:border-outline-gray-3"
|
||||
@click="saveSlot(slot, row)"
|
||||
:class="{
|
||||
'border-outline-gray-4 text-ink-gray-9':
|
||||
evaluation.date == row.date &&
|
||||
evaluation.start_time == slot.start_time,
|
||||
}"
|
||||
>
|
||||
{{ formatTime(slot.start_time) }} -
|
||||
{{ formatTime(slot.end_time) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="evaluation.course && evaluation.date"
|
||||
class="text-sm italic text-ink-red-4"
|
||||
>
|
||||
{{ __('No slots available for this date.') }}
|
||||
<div v-else class="text-ink-red-3">
|
||||
{{ __('No slots available for the selected course.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -67,14 +64,15 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
call,
|
||||
createResource,
|
||||
dayjs,
|
||||
Dialog,
|
||||
createResource,
|
||||
Select,
|
||||
FormControl,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, watch, inject } from 'vue'
|
||||
import { ref, watch, inject } from 'vue'
|
||||
import { Calendar } from 'lucide-vue-next'
|
||||
import { formatTime } from '@/utils/'
|
||||
|
||||
const user = inject('$user')
|
||||
@@ -96,7 +94,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const evaluation = reactive({
|
||||
const evaluation = ref({
|
||||
course: '',
|
||||
date: '',
|
||||
start_time: '',
|
||||
@@ -106,49 +104,28 @@ const evaluation = reactive({
|
||||
member: user.data.name,
|
||||
})
|
||||
|
||||
const createEvaluation = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'LMS Certificate Request',
|
||||
batch_name: values.batch,
|
||||
...values,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function submitEvaluation(close) {
|
||||
createEvaluation.submit(evaluation, {
|
||||
validate() {
|
||||
if (!evaluation.course) {
|
||||
return 'Please select a course.'
|
||||
}
|
||||
if (!evaluation.date) {
|
||||
return 'Please select a date.'
|
||||
}
|
||||
if (!evaluation.start_time) {
|
||||
return 'Please select a slot.'
|
||||
}
|
||||
if (dayjs(evaluation.date).isBefore(dayjs(), 'day')) {
|
||||
return 'Please select a future date.'
|
||||
}
|
||||
if (dayjs(evaluation.date).isAfter(dayjs(props.endDate), 'day')) {
|
||||
return `Please select a date before the end date ${dayjs(
|
||||
props.endDate
|
||||
).format('DD MMMM YYYY')}.`
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
evaluations.value.reload()
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
console.log(err.messages?.[0] || err)
|
||||
toast.warning(__(err.messages?.[0] || err), { duration: 10000 })
|
||||
if (!evaluation.value.date || !evaluation.value.start_time) {
|
||||
toast.warning(__('Please select a slot for your evaluation.'), {
|
||||
duration: 10,
|
||||
})
|
||||
return
|
||||
}
|
||||
call('frappe.client.insert', {
|
||||
doc: {
|
||||
doctype: 'LMS Certificate Request',
|
||||
batch_name: evaluation.value.batch,
|
||||
...evaluation.value,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
evaluations.value.reload()
|
||||
close()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err.messages?.[0] || err)
|
||||
toast.warning(__(err.messages?.[0] || err), { duration: 20 })
|
||||
})
|
||||
}
|
||||
|
||||
const getCourses = () => {
|
||||
@@ -163,7 +140,7 @@ const getCourses = () => {
|
||||
}
|
||||
|
||||
if (courses.length === 1) {
|
||||
evaluation.course = courses[0].value
|
||||
evaluation.value.course = courses[0].value
|
||||
}
|
||||
|
||||
return courses
|
||||
@@ -174,34 +151,22 @@ const slots = createResource({
|
||||
makeParams(values) {
|
||||
return {
|
||||
course: values.course,
|
||||
date: values.date,
|
||||
batch: props.batch,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => evaluation.date,
|
||||
(date) => {
|
||||
evaluation.start_time = ''
|
||||
if (date && evaluation.course) {
|
||||
slots.submit(evaluation)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => evaluation.course,
|
||||
() => evaluation.value.course,
|
||||
(course) => {
|
||||
evaluation.date = ''
|
||||
evaluation.start_time = ''
|
||||
slots.reset()
|
||||
slots.reload(evaluation.value)
|
||||
}
|
||||
)
|
||||
|
||||
const saveSlot = (slot) => {
|
||||
evaluation.start_time = slot.start_time
|
||||
evaluation.end_time = slot.end_time
|
||||
evaluation.day = slot.day
|
||||
const saveSlot = (slot, row) => {
|
||||
evaluation.value.start_time = slot.start_time
|
||||
evaluation.value.end_time = slot.end_time
|
||||
evaluation.value.date = row.date
|
||||
evaluation.value.day = row.day
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<template>
|
||||
<Tooltip :text="user.full_name">
|
||||
<Avatar
|
||||
class="avatar border border-outline-gray-2 cursor-auto"
|
||||
v-if="user"
|
||||
:label="user.full_name"
|
||||
:image="user.user_image"
|
||||
:size="size"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Avatar
|
||||
class="avatar border border-outline-gray-2 cursor-auto"
|
||||
v-if="user"
|
||||
:label="user.full_name"
|
||||
:image="user.user_image"
|
||||
:size="size"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="user.looking_for_job" #indicator>
|
||||
<Tooltip :text="__('Open to Opportunities')" placement="right">
|
||||
<div class="rounded-full bg-surface-green-3 w-fit">
|
||||
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Avatar>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Avatar, Tooltip } from 'frappe-ui'
|
||||
import { BadgeCheckIcon } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
@@ -21,4 +30,15 @@ const props = defineProps({
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
|
||||
const checkSize = computed(() => {
|
||||
let sizeMap = {
|
||||
sm: 'size-1',
|
||||
md: 'size-2',
|
||||
lg: 'size-3',
|
||||
xl: 'size-3',
|
||||
'2xl': 'size-3',
|
||||
}
|
||||
return sizeMap[props.size] || 'size-3'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -148,7 +148,7 @@ const assignmentFilter = computed(() => {
|
||||
|
||||
const assignments = createListResource({
|
||||
doctype: 'LMS Assignment',
|
||||
fields: ['name', 'title', 'type', 'creation', 'question'],
|
||||
fields: ['name', 'title', 'type', 'creation', 'question', 'course'],
|
||||
orderBy: 'modified desc',
|
||||
cache: ['assignments'],
|
||||
transform(data) {
|
||||
|
||||
@@ -144,6 +144,20 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="batch.data.evaluation_end_date && isStudent"
|
||||
class="text-sm leading-5 bg-surface-amber-1 text-ink-amber-3 p-2 rounded-md mb-10"
|
||||
>
|
||||
{{ __('The last day to schedule your evaluations is ') }}
|
||||
<span class="font-medium">
|
||||
{{
|
||||
dayjs(batch.data.evaluation_end_date).format('DD MMMM YYYY')
|
||||
}} </span
|
||||
>.
|
||||
{{
|
||||
__('Please make sure to schedule your evaluation before this date.')
|
||||
}}
|
||||
</div>
|
||||
<div v-if="dayjs().isSameOrAfter(dayjs(batch.data.start_date))">
|
||||
<div class="text-ink-gray-7 font-semibold mb-2">
|
||||
{{ __('Feedback') }}
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="participants.data?.length" class="divide-y">
|
||||
<template v-for="participant in participants.data">
|
||||
<div v-if="participants.data?.length" class="">
|
||||
<template v-for="(participant, index) in participants.data">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'ProfileCertificates',
|
||||
@@ -47,15 +47,16 @@
|
||||
username: participant.username,
|
||||
},
|
||||
}"
|
||||
class="flex sm:rounded px-3 py-2 sm:h-15 hover:bg-surface-gray-2"
|
||||
class="flex h-15 rounded-md hover:bg-surface-gray-2 px-3"
|
||||
>
|
||||
<div class="flex items-center w-full space-x-3">
|
||||
<Avatar
|
||||
:image="participant.user_image"
|
||||
class="size-8 rounded-full object-contain"
|
||||
:label="participant.full_name"
|
||||
size="2xl"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center w-full space-x-3 py-2"
|
||||
:class="{
|
||||
'border-b': index < participants.data.length - 1,
|
||||
}"
|
||||
>
|
||||
<UserAvatar :user="participant" size="2xl" />
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full">
|
||||
<div class="flex-1">
|
||||
<div class="text-base font-medium text-ink-gray-8">
|
||||
@@ -115,6 +116,7 @@ import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { GraduationCap } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
|
||||
const currentCategory = ref('')
|
||||
const filters = ref({})
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
route: { name: 'Jobs' },
|
||||
},
|
||||
{
|
||||
label: job.doc?.job_title,
|
||||
route: { name: 'JobDetail', params: { job: job.doc?.name } },
|
||||
label: job.data?.job_title,
|
||||
route: { name: 'JobDetail', params: { job: job.data?.name } },
|
||||
},
|
||||
]"
|
||||
/>
|
||||
@@ -24,7 +24,7 @@
|
||||
v-if="canManageJob && applicationCount.data > 0"
|
||||
:to="{
|
||||
name: 'JobApplications',
|
||||
params: { job: job.doc?.name },
|
||||
params: { job: job.data?.name },
|
||||
}"
|
||||
>
|
||||
<Button variant="subtle">
|
||||
@@ -35,7 +35,7 @@
|
||||
v-if="canManageJob"
|
||||
:to="{
|
||||
name: 'JobForm',
|
||||
params: { jobName: job.doc?.name },
|
||||
params: { jobName: job.data?.name },
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
@@ -45,7 +45,7 @@
|
||||
{{ __('Edit') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button @click="redirectToWebsite(job.doc?.company_website)">
|
||||
<Button @click="redirectToWebsite(job.data?.company_website)">
|
||||
<template #prefix>
|
||||
<SquareArrowOutUpRight class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
@@ -69,30 +69,30 @@
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-else-if="!readOnlyMode">
|
||||
<Button @click="redirectToLogin(job.doc?.name)">
|
||||
<Button @click="redirectToLogin(job.data?.name)">
|
||||
<span>
|
||||
{{ __('Login to apply') }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div v-if="job.doc" class="max-w-3xl mx-auto pt-5">
|
||||
<div v-if="job.data" class="max-w-3xl mx-auto pt-5">
|
||||
<div class="p-4">
|
||||
<div class="space-y-5 mb-12">
|
||||
<div class="flex">
|
||||
<img
|
||||
:src="job.doc.company_logo"
|
||||
:src="job.data.company_logo"
|
||||
class="size-10 rounded-lg object-contain cursor-pointer mr-4"
|
||||
:alt="job.doc.company_name"
|
||||
@click="redirectToWebsite(job.doc.company_website)"
|
||||
:alt="job.data.company_name"
|
||||
@click="redirectToWebsite(job.data.company_website)"
|
||||
/>
|
||||
<div class="">
|
||||
<div class="text-2xl text-ink-gray-9 font-semibold mb-1">
|
||||
{{ job.doc.job_title }}
|
||||
{{ job.data.job_title }}
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5 font-semibold">
|
||||
{{ job.doc.company_name }} - {{ job.doc.location }},
|
||||
{{ job.doc.country }}
|
||||
{{ job.data.company_name }} - {{ job.data.location }},
|
||||
{{ job.data.country }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,19 +102,19 @@
|
||||
<template #prefix>
|
||||
<CalendarDays class="size-3 stroke-2 text-ink-gray-7" />
|
||||
</template>
|
||||
{{ dayjs(job.doc.creation).fromNow() }}
|
||||
{{ dayjs(job.data.creation).fromNow() }}
|
||||
</Badge>
|
||||
<Badge size="lg">
|
||||
<template #prefix>
|
||||
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
|
||||
</template>
|
||||
{{ job.doc.type }}
|
||||
{{ job.data.type }}
|
||||
</Badge>
|
||||
<Badge v-if="job.doc?.work_mode" size="lg">
|
||||
<Badge v-if="job.data?.work_mode" size="lg">
|
||||
<template #prefix>
|
||||
<BriefcaseBusiness class="size-3 stroke-2 text-ink-gray-7" />
|
||||
</template>
|
||||
{{ job.doc.work_mode }}
|
||||
{{ job.data.work_mode }}
|
||||
</Badge>
|
||||
<Badge v-if="applicationCount.data" size="lg">
|
||||
<template #prefix>
|
||||
@@ -137,14 +137,14 @@
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-html="job.doc.description"
|
||||
v-html="job.data.description"
|
||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-12"
|
||||
></p>
|
||||
</div>
|
||||
<JobApplicationModal
|
||||
v-model="showApplicationModal"
|
||||
v-model:application="jobApplication"
|
||||
:job="job.doc.name"
|
||||
:job="job.data.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +155,6 @@ import {
|
||||
Button,
|
||||
Breadcrumbs,
|
||||
createResource,
|
||||
createDocumentResource,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { inject, ref, computed } from 'vue'
|
||||
@@ -187,16 +186,18 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const job = createDocumentResource({
|
||||
doctype: 'Job Opportunity',
|
||||
name: props.job,
|
||||
auto: true,
|
||||
const job = createResource({
|
||||
url: 'lms.lms.api.get_job_details',
|
||||
params: {
|
||||
job: props.job,
|
||||
},
|
||||
cache: ['job', props.job],
|
||||
auto: true,
|
||||
onSuccess: (data) => {
|
||||
if (user.data?.name) {
|
||||
jobApplication.submit()
|
||||
applicationCount.submit()
|
||||
}
|
||||
applicationCount.submit()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -206,7 +207,7 @@ const jobApplication = createResource({
|
||||
return {
|
||||
doctype: 'LMS Job Application',
|
||||
filters: {
|
||||
job: job.doc?.name,
|
||||
job: job.data?.name,
|
||||
user: user.data?.name,
|
||||
},
|
||||
}
|
||||
@@ -219,7 +220,7 @@ const applicationCount = createResource({
|
||||
return {
|
||||
doctype: 'LMS Job Application',
|
||||
filters: {
|
||||
job: job.doc?.name,
|
||||
job: job.data?.name,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -238,13 +239,13 @@ const redirectToWebsite = (url) => {
|
||||
}
|
||||
|
||||
const canManageJob = computed(() => {
|
||||
if (!user.data?.name || !job.doc) return false
|
||||
return user.data.name === job.doc.owner || user.data?.is_moderator
|
||||
if (!user.data?.name || !job.data) return false
|
||||
return user.data.name === job.data.owner || user.data?.is_moderator
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: job.doc?.job_title,
|
||||
title: job.data?.job_title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -137,6 +137,10 @@ const isModerator = computed(() => {
|
||||
})
|
||||
|
||||
const getClosedJobCount = () => {
|
||||
if (!user.data?.name) {
|
||||
return
|
||||
}
|
||||
|
||||
const filters = {
|
||||
status: 'Closed',
|
||||
}
|
||||
|
||||
@@ -342,6 +342,7 @@ import {
|
||||
TabButtons,
|
||||
Tooltip,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
@@ -799,6 +800,10 @@ const enrollStudent = () => {
|
||||
onSuccess() {
|
||||
window.location.reload()
|
||||
},
|
||||
onError(err) {
|
||||
toast.error(__(err.messages?.[0] || err))
|
||||
console.error(err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,24 +50,51 @@
|
||||
<div class="mx-auto -mt-10 md:-mt-4 max-w-4xl translate-x-0 px-5">
|
||||
<div class="flex flex-col md:flex-row items-center">
|
||||
<div>
|
||||
<img
|
||||
v-if="profile.data.user_image"
|
||||
:src="profile.data.user_image"
|
||||
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
|
||||
/>
|
||||
<UserAvatar
|
||||
v-else
|
||||
:user="profile.data"
|
||||
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
|
||||
/>
|
||||
<div class="relative">
|
||||
<img
|
||||
v-if="profile.data.user_image"
|
||||
:src="profile.data.user_image"
|
||||
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
|
||||
/>
|
||||
<Tooltip
|
||||
v-if="profile.data.looking_for_job"
|
||||
:text="__('Open to Opportunities')"
|
||||
placement="right"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-3 right-1 p-0.5 bg-surface-white rounded-full"
|
||||
>
|
||||
<div class="rounded-full bg-surface-green-3 w-fit">
|
||||
<BadgeCheckIcon class="text-ink-white size-5" />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-6">
|
||||
<h2 class="mt-2 text-3xl font-semibold text-ink-gray-9">
|
||||
<div class="ml-6 mt-5">
|
||||
<h2 class="text-3xl font-semibold text-ink-gray-9">
|
||||
{{ profile.data.full_name }}
|
||||
</h2>
|
||||
<div class="mt-2 text-base text-ink-gray-7">
|
||||
<div class="text-base text-ink-gray-7 mt-1">
|
||||
{{ profile.data.headline }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 mt-2">
|
||||
<Twitter
|
||||
v-if="profile.data.twitter"
|
||||
class="size-4 text-ink-gray-5 cursor-pointer"
|
||||
@click="navigateTo(profile.data.twitter)"
|
||||
/>
|
||||
<Linkedin
|
||||
v-if="profile.data.linkedin"
|
||||
class="size-4 text-ink-gray-5 cursor-pointer"
|
||||
@click="navigateTo(profile.data.linkedin)"
|
||||
/>
|
||||
<Github
|
||||
v-if="profile.data.github"
|
||||
class="size-4 text-ink-gray-5 cursor-pointer"
|
||||
@click="navigateTo(profile.data.github)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="isSessionUser() && !readOnlyMode"
|
||||
@@ -81,7 +108,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-6">
|
||||
<div class="mb-4 mt-10">
|
||||
<TabButtons
|
||||
class="inline-block"
|
||||
:buttons="getTabButtons()"
|
||||
@@ -104,11 +131,19 @@ import {
|
||||
call,
|
||||
createResource,
|
||||
TabButtons,
|
||||
Tooltip,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Edit, RefreshCcw } from 'lucide-vue-next'
|
||||
import {
|
||||
BadgeCheckIcon,
|
||||
Edit,
|
||||
Github,
|
||||
Linkedin,
|
||||
RefreshCcw,
|
||||
Twitter,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
@@ -229,6 +264,10 @@ const reloadUser = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const navigateTo = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let crumbs = [
|
||||
{
|
||||
|
||||
@@ -56,11 +56,13 @@
|
||||
</template>
|
||||
<template #body-main>
|
||||
<div class="w-[250px] text-base">
|
||||
<img
|
||||
:src="badge.badge_image"
|
||||
:alt="badge.badge"
|
||||
class="bg-surface-gray-2 rounded-t-md h-[200px] mx-auto"
|
||||
/>
|
||||
<div class="bg-surface-gray-2 rounded-t-md py-5">
|
||||
<img
|
||||
:src="badge.badge_image"
|
||||
:alt="badge.badge"
|
||||
class="h-[200px] mx-auto"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="text-2xl font-semibold mb-2">
|
||||
{{ badge.badge }}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<div v-if="program.data" class="text-base">
|
||||
<div v-if="program.data" class="text-base text-ink-gray-9">
|
||||
<div class="bg-surface-blue-2 text-ink-blue-3 p-2 rounded-md leading-5">
|
||||
<span>
|
||||
{{
|
||||
@@ -46,9 +46,9 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||
<div
|
||||
v-for="course in program.data.courses"
|
||||
class="flex flex-col border p-2 rounded-md h-full"
|
||||
class="flex flex-col border border-outline-gray-2 p-2 rounded-md h-full"
|
||||
>
|
||||
<div class="font-semibold leading-5 mb-2">
|
||||
<div class="font-semibold text-ink-gray-9 leading-5 mb-2">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
<div class="flex items-center space-x-1 mt-auto">
|
||||
<UserAvatar :user="course.instructors[0]" />
|
||||
<span>
|
||||
<span class="text-ink-gray-9">
|
||||
{{ course.instructors[0].full_name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@click="openDetails(program.name, category)"
|
||||
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer"
|
||||
>
|
||||
<div class="text-lg font-semibold mb-2">
|
||||
<div class="text-lg font-semibold text-ink-gray-9 mb-2">
|
||||
{{ program.name }}
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<div v-if="Object.keys(program).includes('progress')" class="mt-5">
|
||||
<ProgressBar :progress="program.progress" />
|
||||
<div class="text-sm mt-1">
|
||||
<div class="text-sm text-ink-gray-7 mt-1">
|
||||
{{ Math.ceil(program.progress) }}% {{ __('completed') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,10 @@
|
||||
</Tooltip>
|
||||
<div class="space-y-1 w-full">
|
||||
<div class="flex items-center">
|
||||
<div class="font-medium" v-html="result.title"></div>
|
||||
<div
|
||||
class="font-medium text-ink-gray-9"
|
||||
v-html="result.title"
|
||||
></div>
|
||||
<div class="text-sm text-ink-gray-5 ml-2">
|
||||
{{ getDocTypeTitle(result.doctype) }}
|
||||
</div>
|
||||
@@ -89,7 +92,10 @@
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="leading-5" v-html="result.content"></div>
|
||||
<div
|
||||
class="leading-5 text-ink-gray-7"
|
||||
v-html="result.content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import AssessmentPlugin from '@/components/AssessmentPlugin.vue'
|
||||
import translationPlugin from '../translation'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { call } from 'frappe-ui'
|
||||
import router from '@/router'
|
||||
|
||||
export class Assignment {
|
||||
constructor({ data, api, readOnly }) {
|
||||
@@ -84,6 +85,7 @@ export class Assignment {
|
||||
},
|
||||
})
|
||||
app.use(translationPlugin)
|
||||
app.use(router)
|
||||
app.mount(this.wrapper)
|
||||
}
|
||||
|
||||
|
||||
+79
-63
@@ -1,70 +1,86 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import frappeui from 'frappe-ui/vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [
|
||||
frappeui({
|
||||
frappeProxy: true,
|
||||
lucideIcons: true,
|
||||
jinjaBootData: true,
|
||||
frappeTypes: {
|
||||
input: {},
|
||||
},
|
||||
buildConfig: {
|
||||
indexHtmlPath: '../lms/www/lms.html',
|
||||
},
|
||||
}),
|
||||
vue({
|
||||
script: {
|
||||
defineModel: true,
|
||||
propsDestructure: true,
|
||||
},
|
||||
}),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
},
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
globDirectory: '/assets/lms/frontend',
|
||||
globPatterns: ['**/*.{js,ts,css,html,png,svg}'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ request }) =>
|
||||
request.destination === 'document',
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'html-cache',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: false,
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0', // Accept connections from any network interface
|
||||
allowedHosts: ['ps', 'fs', 'home'], // Explicitly allow this host
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
const isDev = mode === 'development'
|
||||
const frappeui = await importFrappeUIPlugin(isDev)
|
||||
|
||||
const config = {
|
||||
define: {
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'feather-icons',
|
||||
'engine.io-client',
|
||||
'interactjs',
|
||||
'highlight.js',
|
||||
'plyr',
|
||||
plugins: [
|
||||
frappeui({
|
||||
frappeProxy: true,
|
||||
lucideIcons: true,
|
||||
jinjaBootData: true,
|
||||
buildConfig: {
|
||||
indexHtmlPath: '../lms/www/lms.html',
|
||||
},
|
||||
}),
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
globDirectory: '/assets/lms/frontend',
|
||||
globPatterns: ['**/*.{js,ts,css,html,png,svg}'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ request }) =>
|
||||
request.destination === 'document',
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'html-cache',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: false,
|
||||
}),
|
||||
],
|
||||
exclude: mode === 'production' ? [] : ['frappe-ui'],
|
||||
},
|
||||
}))
|
||||
server: {
|
||||
host: '0.0.0.0', // Accept connections from any network interface
|
||||
allowedHosts: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'feather-icons',
|
||||
'tailwind.config.js',
|
||||
'interactjs',
|
||||
'highlight.js',
|
||||
'plyr',
|
||||
],
|
||||
exclude: mode === 'production' ? [] : ['frappe-ui'],
|
||||
},
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
async function importFrappeUIPlugin(isDev) {
|
||||
if (isDev) {
|
||||
try {
|
||||
const module = await import('../frappe-ui/vite')
|
||||
return module.default
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Local frappe-ui not found, falling back to npm package:',
|
||||
error.message
|
||||
)
|
||||
}
|
||||
}
|
||||
// Fall back to npm package if local import fails
|
||||
const module = await import('frappe-ui/vite')
|
||||
return module.default
|
||||
}
|
||||
|
||||
-5561
File diff suppressed because it is too large
Load Diff
@@ -250,10 +250,10 @@
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "interest",
|
||||
"insert_after": "verify_terms",
|
||||
"is_system_generated": 1,
|
||||
"is_virtual": 0,
|
||||
"label": "I am looking for a job",
|
||||
"label": "Open to Opportunities",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
@@ -406,7 +406,7 @@
|
||||
"dt": "User",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "medium",
|
||||
"fieldname": "twitter",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
@@ -421,6 +421,62 @@
|
||||
"insert_after": "github",
|
||||
"is_system_generated": 1,
|
||||
"is_virtual": 0,
|
||||
"label": "Twitter ID",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2025-12-15 14:46:55.834145",
|
||||
"module": null,
|
||||
"name": "User-twitter",
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"options": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"show_dashboard": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 1,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Custom Field",
|
||||
"dt": "User",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "medium",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "twitter",
|
||||
"is_system_generated": 1,
|
||||
"is_virtual": 0,
|
||||
"label": "Medium ID",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
|
||||
+28
-1
@@ -147,6 +147,29 @@ def verify_billing_access(doctype, name, billing_type):
|
||||
return access, message
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_job_details(job):
|
||||
return frappe.db.get_value(
|
||||
"Job Opportunity",
|
||||
job,
|
||||
[
|
||||
"job_title",
|
||||
"location",
|
||||
"country",
|
||||
"type",
|
||||
"work_mode",
|
||||
"company_name",
|
||||
"company_logo",
|
||||
"company_website",
|
||||
"name",
|
||||
"creation",
|
||||
"description",
|
||||
"owner",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_job_opportunities(filters=None, orFilters=None):
|
||||
if not filters:
|
||||
@@ -286,7 +309,7 @@ def get_certified_participants(filters=None, start=0, page_length=100):
|
||||
details = frappe.db.get_value(
|
||||
"User",
|
||||
participant.member,
|
||||
["full_name", "user_image", "username", "country", "headline"],
|
||||
["full_name", "user_image", "username", "country", "headline", "looking_for_job"],
|
||||
as_dict=1,
|
||||
)
|
||||
details["certificate_count"] = count
|
||||
@@ -1612,6 +1635,10 @@ def get_profile_details(username):
|
||||
"headline",
|
||||
"language",
|
||||
"cover_image",
|
||||
"looking_for_job",
|
||||
"linkedin",
|
||||
"github",
|
||||
"twitter",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_time, getdate
|
||||
from frappe.utils import add_days, get_time, getdate, nowdate
|
||||
|
||||
from lms.lms.utils import get_evaluator
|
||||
|
||||
@@ -58,33 +58,125 @@ class CourseEvaluator(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_schedule(course, date, batch=None):
|
||||
def get_schedule(course, batch=None):
|
||||
evaluator = get_evaluator(course, batch)
|
||||
day = datetime.strptime(date, "%Y-%m-%d").strftime("%A")
|
||||
start_date = nowdate()
|
||||
end_date = get_schedule_range_end_date(start_date, batch)
|
||||
all_slots = get_all_slots(evaluator, start_date, end_date)
|
||||
booked_slots = get_booked_slots(evaluator, start_date, end_date)
|
||||
all_slots = remove_booked_slots(all_slots, booked_slots)
|
||||
return all_slots
|
||||
|
||||
all_slots = frappe.get_all(
|
||||
|
||||
def get_all_slots(evaluator, start_date, end_date):
|
||||
schedule = get_evaluator_schedule(evaluator)
|
||||
unavailable_dates = get_unavailable_dates(evaluator)
|
||||
all_slots = []
|
||||
current_date = getdate(start_date)
|
||||
end_date = getdate(end_date)
|
||||
|
||||
while current_date <= end_date:
|
||||
if current_date in unavailable_dates:
|
||||
current_date = add_days(current_date, 1)
|
||||
continue
|
||||
day_of_week = current_date.strftime("%A")
|
||||
slots_for_day = [x for x in schedule if x.day == day_of_week]
|
||||
for slot in slots_for_day:
|
||||
all_slots.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"day": day_of_week,
|
||||
"date": current_date,
|
||||
"start_time": slot.start_time,
|
||||
"end_time": slot.end_time,
|
||||
}
|
||||
)
|
||||
)
|
||||
current_date = add_days(current_date, 1)
|
||||
return all_slots
|
||||
|
||||
|
||||
def get_evaluator_schedule(evaluator):
|
||||
return frappe.get_all(
|
||||
"Evaluator Schedule",
|
||||
filters={
|
||||
"parent": evaluator,
|
||||
"day": day,
|
||||
},
|
||||
fields=["day", "start_time", "end_time"],
|
||||
order_by="start_time",
|
||||
)
|
||||
|
||||
booked_slots = frappe.get_all(
|
||||
|
||||
def get_booked_slots(evaluator, start_date, end_date):
|
||||
date = ["between", [start_date, end_date]]
|
||||
return frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
filters={
|
||||
"evaluator": evaluator,
|
||||
"date": date,
|
||||
"status": ["!=", "Cancelled"],
|
||||
},
|
||||
fields=["start_time", "day"],
|
||||
fields=["start_time", "day", "date"],
|
||||
)
|
||||
|
||||
for slot in booked_slots:
|
||||
same_slot = [x for x in all_slots if x.start_time == slot.start_time and x.day == slot.day]
|
||||
if len(same_slot):
|
||||
all_slots.remove(same_slot[0])
|
||||
|
||||
return all_slots
|
||||
def remove_booked_slots(all_slots, booked_slots):
|
||||
slots_to_remove = []
|
||||
for slot in all_slots:
|
||||
for booked in booked_slots:
|
||||
if slot.date == booked.date and slot.start_time == booked.start_time:
|
||||
slots_to_remove.append(slot)
|
||||
|
||||
for slot in slots_to_remove:
|
||||
all_slots.remove(slot)
|
||||
|
||||
return group_slots_by_date(all_slots)
|
||||
|
||||
|
||||
def group_slots_by_date(all_slots):
|
||||
slots_by_date = []
|
||||
dates_included = set()
|
||||
for slot in all_slots:
|
||||
date_str = slot.get("date").strftime("%Y-%m-%d")
|
||||
if date_str not in dates_included:
|
||||
slots_by_date.append({"date": date_str, "day": slot.day, "slots": []})
|
||||
dates_included.add(date_str)
|
||||
|
||||
for date_slot in slots_by_date:
|
||||
if date_slot.get("date") == date_str:
|
||||
date_slot.get("slots").append(
|
||||
{
|
||||
"start_time": slot.get("start_time"),
|
||||
"end_time": slot.get("end_time"),
|
||||
}
|
||||
)
|
||||
return slots_by_date
|
||||
|
||||
|
||||
def get_evaluator_availability(evaluator):
|
||||
return frappe.db.get_value(
|
||||
"Course Evaluator", evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
|
||||
)
|
||||
|
||||
|
||||
def get_unavailable_dates(evaluator):
|
||||
availability = get_evaluator_availability(evaluator)
|
||||
unavailable_dates = []
|
||||
if availability.unavailable_from and availability.unavailable_to:
|
||||
current_date = getdate(availability.unavailable_from)
|
||||
end_date = getdate(availability.unavailable_to)
|
||||
|
||||
while current_date <= end_date:
|
||||
unavailable_dates.append(current_date)
|
||||
current_date = add_days(current_date, 1)
|
||||
return unavailable_dates
|
||||
|
||||
|
||||
def get_schedule_range_end_date(start_date, batch=None):
|
||||
end_date = add_days(start_date, 60)
|
||||
if batch:
|
||||
batch_end_date = frappe.db.get_value("LMS Batch", batch, "evaluation_end_date")
|
||||
if batch_end_date and batch_end_date < getdate(end_date):
|
||||
end_date = getdate(batch_end_date)
|
||||
|
||||
return end_date
|
||||
|
||||
@@ -3,7 +3,62 @@
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.utils import add_days, format_time, getdate
|
||||
|
||||
from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule
|
||||
from lms.lms.test_utils import TestUtils
|
||||
|
||||
|
||||
class TestCourseEvaluator(UnitTestCase):
|
||||
pass
|
||||
def setUp(self):
|
||||
self.admin = TestUtils.create_user(
|
||||
self, "frappe@example.com", "Frappe", "Admin", ["Moderator", "Course Creator", "Batch Evaluator"]
|
||||
)
|
||||
self.course = TestUtils.create_a_course(self)
|
||||
|
||||
self.evaluator = TestUtils.create_evaluator(self)
|
||||
self.batch = TestUtils.create_a_batch(self)
|
||||
|
||||
def test_schedule_day_and_time(self):
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
days = ["Monday", "Wednesday"]
|
||||
self.assertGreaterEqual(len(schedule), 14)
|
||||
for row in schedule:
|
||||
self.assertIn(row.get("day"), days)
|
||||
if row.get("day") == "Monday":
|
||||
for slot in row.get("slots"):
|
||||
self.assertEqual(format_time(slot.get("start_time"), "HH:mm:ss"), "10:00:00")
|
||||
self.assertEqual(format_time(slot.get("end_time"), "HH:mm:ss"), "12:00:00")
|
||||
if row.get("day") == "Wednesday":
|
||||
for slot in row.get("slots"):
|
||||
self.assertEqual(format_time(slot.get("start_time"), "HH:mm:ss"), "14:00:00")
|
||||
self.assertEqual(format_time(slot.get("end_time"), "HH:mm:ss"), "16:00:00")
|
||||
|
||||
def test_schedule_dates(self):
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
first_date = self.calculated_first_date_of_schedule()
|
||||
last_date = self.calculated_last_date_of_schedule(first_date)
|
||||
self.assertEqual(getdate(schedule[0].get("date")), first_date)
|
||||
self.assertEqual(getdate(schedule[-1].get("date")), last_date)
|
||||
|
||||
def calculated_first_date_of_schedule(self):
|
||||
today = getdate()
|
||||
offset_monday = (0 - today.weekday() + 7) % 7 # 0 for Monday
|
||||
offset_wednesday = (2 - today.weekday() + 7) % 7 # 2 for Wednesday
|
||||
if offset_monday < offset_wednesday:
|
||||
first_date = add_days(today, offset_monday)
|
||||
else:
|
||||
first_date = add_days(today, offset_wednesday)
|
||||
return first_date
|
||||
|
||||
def calculated_last_date_of_schedule(self, first_date):
|
||||
last_date = add_days(first_date, 56) # 8 weeks course
|
||||
return last_date
|
||||
|
||||
def test_unavailability_dates(self):
|
||||
unavailable_from = getdate(self.evaluator.unavailable_from)
|
||||
unavailable_to = getdate(self.evaluator.unavailable_to)
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
for row in schedule:
|
||||
schedule_date = getdate(row.get("date"))
|
||||
self.assertFalse(unavailable_from < schedule_date < unavailable_to)
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"question",
|
||||
"column_break_hmwv",
|
||||
"type",
|
||||
"grade_assignment",
|
||||
"course",
|
||||
"column_break_hmwv",
|
||||
"question",
|
||||
"section_break_sjti",
|
||||
"show_answer",
|
||||
"answer"
|
||||
@@ -68,12 +69,18 @@
|
||||
{
|
||||
"fieldname": "section_break_sjti",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-10 11:40:38.157448",
|
||||
"modified": "2025-12-19 16:30:58.531722",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment",
|
||||
@@ -112,6 +119,28 @@
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-07-14 10:24:23.526176",
|
||||
"modified": "2025-12-17 14:47:22.944223",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment Submission",
|
||||
@@ -195,7 +195,6 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
@@ -207,7 +206,6 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
|
||||
@@ -377,9 +377,13 @@
|
||||
{
|
||||
"link_doctype": "LMS Certificate",
|
||||
"link_fieldname": "batch_name"
|
||||
},
|
||||
{
|
||||
"link_doctype": "LMS Payment",
|
||||
"link_fieldname": "payment_for_document"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-04 12:54:11.190967",
|
||||
"modified": "2025-12-23 11:27:00.424331",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
|
||||
@@ -48,8 +48,12 @@ class LMSBatchEnrollment(Document):
|
||||
self.payment = payment
|
||||
|
||||
def validate_self_enrollment(self):
|
||||
allow_self_enrollment = frappe.db.get_value("LMS Batch", self.batch, "allow_self_enrollment")
|
||||
if not allow_self_enrollment and not self.is_admin():
|
||||
batch_details = frappe.db.get_value(
|
||||
"LMS Batch", self.batch, ["allow_self_enrollment", "paid_batch"], as_dict=True
|
||||
)
|
||||
if batch_details.paid_batch:
|
||||
return
|
||||
if not batch_details.allow_self_enrollment and not self.is_admin():
|
||||
frappe.throw(_("Enrollment in this batch is restricted. Please contact the Administrator."))
|
||||
|
||||
def is_admin(self):
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-07 19:24:12.272810",
|
||||
"modified": "2025-12-17 16:50:31.128747",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
@@ -164,6 +164,39 @@
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -67,6 +67,7 @@ class LMSCertificateRequest(Document):
|
||||
{
|
||||
"evaluator": self.evaluator,
|
||||
"date": self.date,
|
||||
"status": ["!=", "Cancelled"],
|
||||
"start_time": self.start_time,
|
||||
"member": ["!=", self.member],
|
||||
},
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-23 12:50:13.622277",
|
||||
"modified": "2025-12-23 11:11:23.908797",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
|
||||
@@ -72,20 +72,3 @@ def update_program_progress(member):
|
||||
|
||||
average_progress = ceil(total_progress / len(courses))
|
||||
frappe.db.set_value("LMS Program Member", program.name, "progress", average_progress)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.update(
|
||||
{
|
||||
"doctype": "LMS Enrollment",
|
||||
"batch_old": batch,
|
||||
"course": course,
|
||||
"role": role,
|
||||
"member_type": member_type,
|
||||
"member": member or frappe.session.user,
|
||||
}
|
||||
)
|
||||
enrollment.insert()
|
||||
return enrollment
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "https://falcon.frappe.io/",
|
||||
"fieldname": "livecode_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "LiveCode URL"
|
||||
@@ -451,7 +452,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-10 17:36:15.689695",
|
||||
"modified": "2025-12-22 11:30:13.868031",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
+57
-9
@@ -1,12 +1,13 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified
|
||||
|
||||
from .utils import (
|
||||
get_average_rating,
|
||||
get_chapters,
|
||||
get_evaluator,
|
||||
get_instructors,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
@@ -23,7 +24,7 @@ from .utils import (
|
||||
)
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
class TestUtils(UnitTestCase):
|
||||
def setUp(self):
|
||||
self.student1 = self.create_user("student1@example.com", "Ashley", "Smith", ["LMS Student"])
|
||||
self.student2 = self.create_user("student2@example.com", "John", "Doe", ["LMS Student"])
|
||||
@@ -31,7 +32,7 @@ class TestUtils(unittest.TestCase):
|
||||
"frappe@example.com", "Frappe", "Admin", ["Moderator", "Course Creator", "Batch Evaluator"]
|
||||
)
|
||||
|
||||
self.create_a_course()
|
||||
self.course = self.create_a_course()
|
||||
self.add_chapters()
|
||||
self.add_lessons()
|
||||
|
||||
@@ -43,7 +44,14 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
self.create_certificate(self.course.name, self.student1.email)
|
||||
|
||||
self.evaluator = self.create_evaluator()
|
||||
self.batch = self.create_a_batch()
|
||||
|
||||
def create_a_course(self):
|
||||
existing_course = frappe.db.exists("LMS Course", {"title": "Utility Course"})
|
||||
if existing_course:
|
||||
return frappe.get_doc("LMS Course", existing_course)
|
||||
|
||||
course = frappe.new_doc("LMS Course")
|
||||
course.title = "Utility Course"
|
||||
course.short_introduction = "A course to test utilities of Frappe Learning"
|
||||
@@ -52,7 +60,7 @@ class TestUtils(unittest.TestCase):
|
||||
course.published = 1
|
||||
course.append("instructors", {"instructor": "frappe@example.com"})
|
||||
course.save()
|
||||
self.course = course
|
||||
return course
|
||||
|
||||
def add_chapters(self):
|
||||
chapters = []
|
||||
@@ -66,7 +74,6 @@ class TestUtils(unittest.TestCase):
|
||||
self.course.reload()
|
||||
for chapter in chapters:
|
||||
self.course.append("chapters", {"chapter": chapter.name})
|
||||
|
||||
self.course.save()
|
||||
|
||||
def add_lessons(self):
|
||||
@@ -87,8 +94,43 @@ class TestUtils(unittest.TestCase):
|
||||
chapterDoc.append("lessons", {"lesson": lesson.name})
|
||||
chapterDoc.save()
|
||||
|
||||
def create_evaluator(self):
|
||||
if frappe.db.exists("Course Evaluator", "frappe@example.com"):
|
||||
return frappe.get_doc("Course Evaluator", "frappe@example.com")
|
||||
|
||||
evaluator = frappe.new_doc("Course Evaluator")
|
||||
evaluator.evaluator = "frappe@example.com"
|
||||
evaluator.append("schedule", {"day": "Monday", "start_time": "10:00", "end_time": "12:00"})
|
||||
evaluator.append("schedule", {"day": "Wednesday", "start_time": "14:00", "end_time": "16:00"})
|
||||
evaluator.unavailable_from = add_days(nowdate(), 5)
|
||||
evaluator.unavailable_to = add_days(nowdate(), 12)
|
||||
evaluator.save()
|
||||
return evaluator
|
||||
|
||||
def create_a_batch(self):
|
||||
existing_batch = frappe.db.exists("LMS Batch", {"title": "Utility Training"})
|
||||
if existing_batch:
|
||||
return frappe.get_doc("LMS Batch", existing_batch)
|
||||
|
||||
batch = frappe.new_doc("LMS Batch")
|
||||
batch.title = "Utility Training"
|
||||
batch.start_date = nowdate()
|
||||
batch.end_date = add_days(batch.start_date, 10)
|
||||
batch.start_time = "09:00:00"
|
||||
batch.end_time = "11:00:00"
|
||||
batch.timezone = "Asia/Kolkata"
|
||||
batch.description = "Batch for Utility Course Training"
|
||||
batch.batch_details = "This batch is created to test utility functions."
|
||||
batch.evaluation_end_date = add_days(nowdate(), 120)
|
||||
batch.append("instructors", {"instructor": "frappe@example.com"})
|
||||
batch.append("courses", {"course": self.course.name, "evaluator": "frappe@example.com"})
|
||||
batch.save()
|
||||
return batch
|
||||
|
||||
def create_user(self, email, first_name, last_name, roles):
|
||||
if not frappe.db.exists("User", email):
|
||||
if frappe.db.exists("User", email):
|
||||
return frappe.get_doc("User", email)
|
||||
else:
|
||||
user = frappe.new_doc("User")
|
||||
user.email = email
|
||||
user.first_name = first_name
|
||||
@@ -98,8 +140,6 @@ class TestUtils(unittest.TestCase):
|
||||
user.append("roles", {"role": role})
|
||||
user.save()
|
||||
return user
|
||||
else:
|
||||
return frappe.get_doc("User", email)
|
||||
|
||||
def create_certificate(self, course_name, member):
|
||||
certificate = frappe.new_doc("LMS Certificate")
|
||||
@@ -232,7 +272,14 @@ class TestUtils(unittest.TestCase):
|
||||
frappe.session.user = "Administrator"
|
||||
frappe.delete_doc("User", student3.email)
|
||||
|
||||
def test_get_evaluator(self):
|
||||
evaluator_email = get_evaluator(self.course.name, self.batch.name)
|
||||
self.assertEqual(evaluator_email, self.evaluator.evaluator)
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.db.exists("LMS Batch", self.batch.name):
|
||||
frappe.delete_doc("LMS Batch", self.batch.name)
|
||||
|
||||
if frappe.db.exists("LMS Course", self.course.name):
|
||||
frappe.db.delete("LMS Certificate", {"course": self.course.name})
|
||||
frappe.db.delete("LMS Enrollment", {"course": self.course.name})
|
||||
@@ -242,6 +289,7 @@ class TestUtils(unittest.TestCase):
|
||||
frappe.db.delete("Course Instructor", {"parent": self.course.name})
|
||||
frappe.delete_doc("LMS Course", self.course.name)
|
||||
|
||||
frappe.delete_doc("Course Evaluator", self.evaluator.name)
|
||||
frappe.delete_doc("User", "student1@example.com")
|
||||
frappe.delete_doc("User", "student2@example.com")
|
||||
frappe.delete_doc("User", "frappe@example.com")
|
||||
|
||||
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
+217
-457
File diff suppressed because it is too large
Load Diff
+218
-458
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user