Merge pull request #1010 from pateljannat/evaluation-calendar
feat: Evaluation and Certification from Learning Portal
This commit is contained in:
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "frappe-ui"]
|
||||
path = frappe-ui
|
||||
url = https://github.com/pateljannat/frappe-ui
|
||||
url = https://github.com/frappe/frappe-ui
|
||||
|
||||
1
frappe-ui
Submodule
1
frappe-ui
Submodule
Submodule frappe-ui added at 8cd9b06a5e
@@ -21,7 +21,7 @@
|
||||
"chart.js": "^4.4.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"feather-icons": "^4.28.0",
|
||||
"frappe-ui": "^0.1.56",
|
||||
"frappe-ui": "^0.1.69",
|
||||
"lucide-vue-next": "^0.383.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"pinia": "^2.0.33",
|
||||
|
||||
@@ -108,6 +108,7 @@ const options = createResource({
|
||||
url: 'frappe.desk.search.search_link',
|
||||
cache: [props.doctype, text.value],
|
||||
method: 'POST',
|
||||
auto: true,
|
||||
params: {
|
||||
txt: text.value,
|
||||
doctype: props.doctype,
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
<template>
|
||||
<div class="flex text-center">
|
||||
<div v-for="index in 5">
|
||||
<Star
|
||||
:class="index <= rating ? 'fill-orange-500' : ''"
|
||||
class="h-6 w-6 fill-gray-400 text-gray-50 mr-1 cursor-pointer"
|
||||
@click="markRating(index)"
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs text-gray-600" v-if="props.label">
|
||||
{{ props.label }}
|
||||
</label>
|
||||
<div class="flex text-center">
|
||||
<div
|
||||
v-for="index in 5"
|
||||
@mouseover="hoveredRating = index"
|
||||
@mouseleave="hoveredRating = 0"
|
||||
>
|
||||
<Star
|
||||
class="fill-gray-400 text-gray-50 stroke-1 mr-1 cursor-pointer"
|
||||
:class="iconClasses(index)"
|
||||
@click="markRating(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -23,10 +32,36 @@ const props = defineProps({
|
||||
type: Number,
|
||||
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'])
|
||||
let rating = ref(props.modelValue)
|
||||
const rating = ref(props.modelValue)
|
||||
const hoveredRating = ref(0)
|
||||
|
||||
let emitChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
@@ -36,4 +71,11 @@ function markRating(index) {
|
||||
emitChange(index)
|
||||
rating.value = index
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
rating.value = newVal
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
378
frontend/src/components/Modals/Event.vue
Normal file
378
frontend/src/components/Modals/Event.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: '2xl',
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex text-base">
|
||||
<div class="flex flex-col w-1/2 p-5">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4 text-sm text-gray-800">
|
||||
<Tooltip :text="__('Email ID')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<User class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.member }}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Course')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.course_title }}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="event.batch_title" :text="__('Batch')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<Users class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.batch_title }}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Date')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<Calendar class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ dayjs(event.date).format('DD MMM YYYY') }}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Time')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<Clock class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ formatTime(event.start_time) }} -
|
||||
{{ formatTime(event.end_time) }}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mt-auto">
|
||||
<Button
|
||||
v-if="certificate.name"
|
||||
@click="openCertificate(certificate)"
|
||||
class="w-full"
|
||||
>
|
||||
<template #prefix>
|
||||
<FileText class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('View Certificate') }}
|
||||
</Button>
|
||||
<Button v-else @click="openCallLink(event.venue)" class="w-full">
|
||||
<template #prefix>
|
||||
<Video class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Join Meeting') }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs :tabs="tabs" v-model="tabIndex" class="border-l w-1/2">
|
||||
<template #default="{ tab }">
|
||||
<div
|
||||
v-if="tab.label == 'Evaluation'"
|
||||
class="flex flex-col space-y-4 p-5"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<Rating v-model="evaluation.rating" :label="__('Rating')" />
|
||||
<FormControl
|
||||
type="select"
|
||||
:options="statusOptions"
|
||||
v-model="evaluation.status"
|
||||
:label="__('Status')"
|
||||
class="w-1/2"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
v-model="evaluation.summary"
|
||||
:label="__('Summary')"
|
||||
:rows="7"
|
||||
/>
|
||||
<Button variant="solid" @click="saveEvaluation()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="flex flex-col space-y-4 p-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="certificate.published"
|
||||
:label="__('Published')"
|
||||
/>
|
||||
<Link
|
||||
v-model="certificate.template"
|
||||
:label="__('Template')"
|
||||
doctype="Print Format"
|
||||
:filters="{
|
||||
doc_type: 'LMS Certificate',
|
||||
}"
|
||||
/>
|
||||
<FormControl
|
||||
type="date"
|
||||
v-model="certificate.issue_date"
|
||||
:label="__('Issue Date')"
|
||||
/>
|
||||
<FormControl
|
||||
type="date"
|
||||
v-model="certificate.expiry_date"
|
||||
:label="__('Expiry Date')"
|
||||
/>
|
||||
<Button variant="solid" @click="saveCertificate()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Dialog,
|
||||
Button,
|
||||
FormControl,
|
||||
createResource,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
Textarea,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
User,
|
||||
Calendar,
|
||||
Clock,
|
||||
Video,
|
||||
BookOpen,
|
||||
FileText,
|
||||
GraduationCap,
|
||||
Users,
|
||||
ClipboardList,
|
||||
} from 'lucide-vue-next'
|
||||
import { inject, reactive, watch, ref, computed } from 'vue'
|
||||
import { formatTime, showToast } from '@/utils'
|
||||
import Rating from '@/components/Controls/Rating.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const dayjs = inject('$dayjs')
|
||||
const tabIndex = ref(0)
|
||||
const showCertification = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
event: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const evaluation = reactive({})
|
||||
|
||||
const certificate = reactive({})
|
||||
|
||||
const defaultTemplate = createResource({
|
||||
url: 'frappe.client.get_value',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Property Setter',
|
||||
fieldname: 'value',
|
||||
filters: {
|
||||
doc_type: 'LMS Certificate',
|
||||
property: 'default_print_format',
|
||||
},
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
certificate.template = data.value
|
||||
},
|
||||
})
|
||||
|
||||
const openCallLink = (link) => {
|
||||
window.open(link, '_blank')
|
||||
}
|
||||
|
||||
const evaluationResource = createResource({
|
||||
url: 'lms.lms.api.save_evaluation_details',
|
||||
makeParams(values) {
|
||||
return {
|
||||
member: props.event.member,
|
||||
course: props.event.course,
|
||||
batch_name: props.event.batch_name,
|
||||
date: props.event.date,
|
||||
start_time: props.event.start_time,
|
||||
end_time: props.event.end_time,
|
||||
status: evaluation.status,
|
||||
rating: evaluation.rating,
|
||||
summary: evaluation.summary,
|
||||
evaluator: props.event.evaluator,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
evaluation.name = data.name
|
||||
},
|
||||
})
|
||||
|
||||
const evaluationDetails = createResource({
|
||||
url: 'frappe.client.get',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'LMS Certificate Evaluation',
|
||||
filters: {
|
||||
member: props.event.member,
|
||||
course: props.event.course,
|
||||
},
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
for (const key in data) {
|
||||
if (key in evaluation) evaluation[key] = data[key]
|
||||
if (key == 'rating') evaluation.rating = data.rating * 5
|
||||
if (evaluation.status == 'Pass') showCertification.value = true
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
})
|
||||
|
||||
const saveEvaluation = () => {
|
||||
evaluationResource.submit(
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (evaluation.status == 'Pass') {
|
||||
showCertification.value = true
|
||||
} else {
|
||||
show.value = false
|
||||
}
|
||||
showToast(__('Success'), __('Evaluation saved successfully'), 'check')
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const certificateResource = createResource({
|
||||
url: 'lms.lms.api.save_certificate_details',
|
||||
makeParams(values) {
|
||||
return {
|
||||
member: props.event.member,
|
||||
course: props.event.course,
|
||||
batch_name: props.event.batch_name,
|
||||
published: certificate.published,
|
||||
issue_date: certificate.issue_date,
|
||||
expiry_date: certificate.expiry_date,
|
||||
template: certificate.template,
|
||||
evaluator: props.event.evaluator,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
certificate.name = data
|
||||
},
|
||||
})
|
||||
|
||||
const certificateDetails = createResource({
|
||||
url: 'frappe.client.get',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'LMS Certificate',
|
||||
filters: {
|
||||
member: props.event.member,
|
||||
course: props.event.course,
|
||||
},
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
for (const key in data) {
|
||||
if (key in certificate) certificate[key] = data[key]
|
||||
certificate.name = data.name
|
||||
showCertification.value = true
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
certificate.template = defaultTemplate.data.value
|
||||
},
|
||||
auto: false,
|
||||
})
|
||||
|
||||
const saveCertificate = () => {
|
||||
certificateResource.submit(
|
||||
{},
|
||||
{
|
||||
onSuccess: () => {
|
||||
showToast(__('Success'), __('Certificate saved successfully'), 'check')
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
watch(show, () => {
|
||||
if (show.value) {
|
||||
evaluation.rating = 0
|
||||
evaluation.status = 'Pending'
|
||||
evaluation.summary = ''
|
||||
evaluationDetails.reload()
|
||||
|
||||
certificate.published = true
|
||||
certificate.issue_date = dayjs().format('YYYY-MM-DD')
|
||||
certificate.expiry_date = null
|
||||
certificate.template = null
|
||||
certificate.name = null
|
||||
certificateDetails.reload()
|
||||
}
|
||||
})
|
||||
|
||||
const openCertificate = (certificate) => {
|
||||
window.open(
|
||||
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||
certificate.name
|
||||
}&format=${encodeURIComponent(certificate.template)}`
|
||||
)
|
||||
}
|
||||
|
||||
const statusOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: 'Pending',
|
||||
label: __('Pending'),
|
||||
},
|
||||
{
|
||||
value: 'In Progress',
|
||||
label: __('In Progress'),
|
||||
},
|
||||
{
|
||||
value: 'Pass',
|
||||
label: __('Pass'),
|
||||
},
|
||||
{
|
||||
value: 'Fail',
|
||||
label: __('Fail'),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
const tabsArray = [
|
||||
{
|
||||
label: __('Evaluation'),
|
||||
icon: ClipboardList,
|
||||
},
|
||||
]
|
||||
|
||||
if (showCertification.value) {
|
||||
tabsArray.push({
|
||||
label: __('Certification'),
|
||||
icon: GraduationCap,
|
||||
})
|
||||
}
|
||||
|
||||
return tabsArray
|
||||
})
|
||||
</script>
|
||||
@@ -24,7 +24,7 @@
|
||||
<div>
|
||||
{{ __('Please login to access this page.') }}
|
||||
</div>
|
||||
<Button variant="solid" @click="redirectToLogin()" class="mt-2">
|
||||
<Button @click="redirectToLogin()" class="mt-4">
|
||||
{{ __('Login') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ const coverImage = createResource({
|
||||
|
||||
const setActiveTab = () => {
|
||||
let fragments = route.path.split('/')
|
||||
let sections = ['certificates', 'roles', 'evaluations']
|
||||
let sections = ['certificates', 'roles', 'slots', 'schedule']
|
||||
sections.forEach((section) => {
|
||||
if (fragments.includes(section)) {
|
||||
activeTab.value = convertToTitleCase(section)
|
||||
@@ -161,7 +161,8 @@ watchEffect(() => {
|
||||
About: { name: 'ProfileAbout' },
|
||||
Certificates: { name: 'ProfileCertificates' },
|
||||
Roles: { name: 'ProfileRoles' },
|
||||
Evaluations: { name: 'ProfileEvaluator' },
|
||||
Slots: { name: 'ProfileEvaluator' },
|
||||
Schedule: { name: 'ProfileEvaluationSchedule' },
|
||||
}[activeTab.value]
|
||||
router.push(route)
|
||||
}
|
||||
@@ -185,8 +186,13 @@ const isSessionUser = () => {
|
||||
const getTabButtons = () => {
|
||||
let buttons = [{ label: 'About' }, { label: 'Certificates' }]
|
||||
if ($user.data?.is_moderator) buttons.push({ label: 'Roles' })
|
||||
if (isSessionUser() && $user.data?.is_evaluator)
|
||||
buttons.push({ label: 'Evaluations' })
|
||||
if (
|
||||
isSessionUser() &&
|
||||
($user.data?.is_evaluator || $user.data?.is_moderator)
|
||||
) {
|
||||
buttons.push({ label: 'Slots' })
|
||||
buttons.push({ label: 'Schedule' })
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
102
frontend/src/pages/ProfileEvaluationSchedule.vue
Normal file
102
frontend/src/pages/ProfileEvaluationSchedule.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="mt-7 mb-20">
|
||||
<div class="flex h-screen flex-col overflow-hidden">
|
||||
<Calendar
|
||||
v-if="evaluations.data?.length"
|
||||
:config="{
|
||||
defaultMode: 'Month',
|
||||
disableModes: ['Day', 'Week'],
|
||||
redundantCellHeight: 100,
|
||||
enableShortcuts: false,
|
||||
}"
|
||||
:events="evaluations.data"
|
||||
@click="(event) => openEvent(event)"
|
||||
>
|
||||
<template #header="{ currentMonthYear, decrement, increment }">
|
||||
<div class="mb-2 flex justify-between">
|
||||
<span class="text-lg font-semibold">
|
||||
{{ currentMonthYear }}
|
||||
</span>
|
||||
<div class="flex gap-x-1">
|
||||
<Button
|
||||
@click="decrement()"
|
||||
variant="ghost"
|
||||
class="h-4 w-4"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
<Button
|
||||
@click="increment()"
|
||||
variant="ghost"
|
||||
class="h-4 w-4"
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Calendar>
|
||||
</div>
|
||||
</div>
|
||||
<Event v-model="showEvent" :event="currentEvent" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { Calendar, createListResource, Button } from 'frappe-ui'
|
||||
import { inject, ref } from 'vue'
|
||||
import Event from '@/components/Modals/Event.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const currentEvent = ref(null)
|
||||
const showEvent = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
profile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const evaluations = createListResource({
|
||||
doctype: 'LMS Certificate Request',
|
||||
filters: {
|
||||
evaluator: user.data?.name,
|
||||
},
|
||||
fields: [
|
||||
'name',
|
||||
'member_name',
|
||||
'member',
|
||||
'course',
|
||||
'course_title',
|
||||
'batch_name',
|
||||
'batch_title',
|
||||
'evaluator',
|
||||
'evaluator_name',
|
||||
'date',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'google_meet_link',
|
||||
],
|
||||
auto: true,
|
||||
orderBy: 'creation desc',
|
||||
limit: 100,
|
||||
cache: ['schedule', user.data?.name],
|
||||
transform(data) {
|
||||
return data.map((d) => {
|
||||
let mappedData = Object.assign({}, d)
|
||||
|
||||
mappedData.title = `${d.member_name}'s Evaluation`
|
||||
mappedData.participant = d.member_name
|
||||
mappedData.id = d.name
|
||||
mappedData.venue = d.google_meet_link
|
||||
mappedData.fromDate = `${d.date} ${d.start_time}`
|
||||
mappedData.toDate = `${d.date} ${d.end_time}`
|
||||
mappedData.color = 'green'
|
||||
|
||||
return mappedData
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const openEvent = (event) => {
|
||||
currentEvent.value = event.calendarEvent
|
||||
showEvent.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -79,9 +79,15 @@ const routes = [
|
||||
},
|
||||
{
|
||||
name: 'ProfileEvaluator',
|
||||
path: 'evaluations',
|
||||
path: 'slots',
|
||||
component: () => import('@/pages/ProfileEvaluator.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ProfileEvaluationSchedule',
|
||||
path: 'schedule',
|
||||
component: () =>
|
||||
import('@/pages/ProfileEvaluationSchedule.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
1489
frontend/yarn.lock
1489
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import time_diff, now_datetime, get_datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -610,3 +611,91 @@ def check_app_permission():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_evaluation_details(
|
||||
member,
|
||||
course,
|
||||
batch_name,
|
||||
evaluator,
|
||||
date,
|
||||
start_time,
|
||||
end_time,
|
||||
status,
|
||||
rating,
|
||||
summary,
|
||||
):
|
||||
"""
|
||||
Save evaluation details for a member against a course.
|
||||
"""
|
||||
evaluation = frappe.db.exists(
|
||||
"LMS Certificate Evaluation", {"member": member, "course": course}
|
||||
)
|
||||
|
||||
details = {
|
||||
"date": date,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"status": status,
|
||||
"rating": rating / 5,
|
||||
"summary": summary,
|
||||
"batch_name": batch_name,
|
||||
}
|
||||
|
||||
if evaluation:
|
||||
frappe.db.set_value("LMS Certificate Evaluation", evaluation, details)
|
||||
return evaluation
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Certificate Evaluation")
|
||||
details.update(
|
||||
{
|
||||
"member": member,
|
||||
"course": course,
|
||||
"evaluator": evaluator,
|
||||
}
|
||||
)
|
||||
doc.update(details)
|
||||
doc.insert()
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_certificate_details(
|
||||
member,
|
||||
course,
|
||||
batch_name,
|
||||
evaluator,
|
||||
issue_date,
|
||||
expiry_date,
|
||||
template,
|
||||
published=True,
|
||||
):
|
||||
"""
|
||||
Save certificate details for a member against a course.
|
||||
"""
|
||||
certificate = frappe.db.exists("LMS Certificate", {"member": member, "course": course})
|
||||
|
||||
details = {
|
||||
"published": published,
|
||||
"issue_date": issue_date,
|
||||
"expiry_date": expiry_date,
|
||||
"template": template,
|
||||
"batch_name": batch_name,
|
||||
}
|
||||
|
||||
if certificate:
|
||||
frappe.db.set_value("LMS Certificate", certificate, details)
|
||||
return certificate
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Certificate")
|
||||
details.update(
|
||||
{
|
||||
"member": member,
|
||||
"course": course,
|
||||
"evaluator": evaluator,
|
||||
}
|
||||
)
|
||||
doc.update(details)
|
||||
doc.insert()
|
||||
return doc.name
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
"template",
|
||||
"published",
|
||||
"section_break_scyf",
|
||||
"expiry_date",
|
||||
"evaluator",
|
||||
"evaluator_name",
|
||||
"column_break_slaw",
|
||||
"expiry_date",
|
||||
"batch_name"
|
||||
],
|
||||
"fields": [
|
||||
@@ -95,11 +97,24 @@
|
||||
{
|
||||
"fieldname": "column_break_slaw",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-07-16 15:29:19.708888",
|
||||
"modified": "2024-09-11 11:37:20.419955",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
"field_order": [
|
||||
"member",
|
||||
"member_name",
|
||||
"column_break_ueht",
|
||||
"course",
|
||||
"batch_name",
|
||||
"section_break_zwfi",
|
||||
"evaluator",
|
||||
"evaluator_name",
|
||||
"column_break_5",
|
||||
"date",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"batch_name",
|
||||
"section_break_6",
|
||||
"rating",
|
||||
"status",
|
||||
@@ -103,11 +107,33 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Batch Name",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-07-16 14:06:11.977666",
|
||||
"modified": "2024-09-11 11:20:06.233491",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Evaluation",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"evaluator_name",
|
||||
"column_break_sjco",
|
||||
"batch_name",
|
||||
"batch_title",
|
||||
"timezone",
|
||||
"section_break_lifi",
|
||||
"date",
|
||||
@@ -38,7 +39,6 @@
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
@@ -46,9 +46,9 @@
|
||||
{
|
||||
"fieldname": "evaluator",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Evaluator",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
@@ -137,11 +137,18 @@
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-09-05 16:28:54.043488",
|
||||
"modified": "2024-09-11 11:19:44.669132",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
|
||||
Reference in New Issue
Block a user