feat: eval and certification flow with purchased certificate
This commit is contained in:
67
frontend/src/components/CertificationLinks.vue
Normal file
67
frontend/src/components/CertificationLinks.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
certification.data &&
|
||||||
|
certification.data.membership &&
|
||||||
|
certification.data.paid_certificate &&
|
||||||
|
user.data?.is_student
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-if="!certification.data.membership.purchased_certificate"
|
||||||
|
:to="{
|
||||||
|
name: 'Billing',
|
||||||
|
params: {
|
||||||
|
type: 'certificate',
|
||||||
|
name: courseName,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<GraduationCap class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Get Certified') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-else-if="!certification.data.membership.certficate"
|
||||||
|
:to="{
|
||||||
|
name: 'CourseCertification',
|
||||||
|
params: {
|
||||||
|
courseName: courseName,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button class="w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<GraduationCap class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Get Certified') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Button, createResource } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { GraduationCap } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const certification = createResource({
|
||||||
|
url: 'lms.lms.api.get_certification_details',
|
||||||
|
params: {
|
||||||
|
course: props.courseName,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: ['certificationData', user.data?.name],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -9,27 +9,29 @@
|
|||||||
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
|
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
|
||||||
{{ course.data.price }}
|
{{ course.data.price }}
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<div v-if="course.data.membership" class="space-y-2">
|
||||||
v-if="course.data.membership"
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Lesson',
|
name: 'Lesson',
|
||||||
params: {
|
params: {
|
||||||
courseName: course.name,
|
courseName: course.name,
|
||||||
chapterNumber: course.data.current_lesson
|
chapterNumber: course.data.current_lesson
|
||||||
? course.data.current_lesson.split('-')[0]
|
? course.data.current_lesson.split('-')[0]
|
||||||
: 1,
|
: 1,
|
||||||
lessonNumber: course.data.current_lesson
|
lessonNumber: course.data.current_lesson
|
||||||
? course.data.current_lesson.split('-')[1]
|
? course.data.current_lesson.split('-')[1]
|
||||||
: 1,
|
: 1,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button variant="solid" size="md" class="w-full">
|
<Button variant="solid" size="md" class="w-full">
|
||||||
<span>
|
<span>
|
||||||
{{ __('Continue Learning') }}
|
{{ __('Continue Learning') }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<CertificationLinks :courseName="course.data.name" />
|
||||||
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
v-else-if="course.data.paid_course"
|
v-else-if="course.data.paid_course"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -133,6 +135,7 @@ import { Button, createResource } from 'frappe-ui'
|
|||||||
import { showToast, formatAmount } from '@/utils/'
|
import { showToast, formatAmount } from '@/utils/'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|||||||
@@ -169,6 +169,11 @@ const getCourses = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (courses.length == 1) {
|
||||||
|
evaluation.course = courses[0].value
|
||||||
|
}
|
||||||
|
|
||||||
return courses
|
return courses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,13 @@
|
|||||||
<div class="text-lg font-semibold">
|
<div class="text-lg font-semibold">
|
||||||
{{ __('Upcoming Evaluations') }}
|
{{ __('Upcoming Evaluations') }}
|
||||||
</div>
|
</div>
|
||||||
<Button @click="openEvalModal">
|
<Button
|
||||||
|
v-if="
|
||||||
|
!upcoming_evals.data?.length ||
|
||||||
|
upcoming_evals.length == courses.length
|
||||||
|
"
|
||||||
|
@click="openEvalModal"
|
||||||
|
>
|
||||||
{{ __('Schedule Evaluation') }}
|
{{ __('Schedule Evaluation') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-ink-gray-5">
|
<div v-else class="text-sm italic text-ink-gray-5">
|
||||||
{{ __('No upcoming evaluations.') }}
|
{{ __('Please schedule an evaluation to get certified.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EvaluationModal
|
<EvaluationModal
|
||||||
|
|||||||
@@ -1,2 +1,116 @@
|
|||||||
<template>Course Certificate</template>
|
<template>
|
||||||
<script setup></script>
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
</header>
|
||||||
|
<div class="p-5">
|
||||||
|
<div v-if="certificate.data">
|
||||||
|
<div class="text-lg text-ink-gray-9 font-semibold mb-1">
|
||||||
|
{{ __('Certification') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-9 text-sm">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'You are already certified for this course. Click on the card below to open your certificate.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="border p-2 w-fit rounded-md space-y-2 hover:bg-surface-gray-1 cursor-pointer mt-5"
|
||||||
|
@click="openCertificate(certificate.data)"
|
||||||
|
>
|
||||||
|
<div class="text-ink-gray-9 font-semibold">
|
||||||
|
{{ courseTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-ink-gray-7 font-medium">
|
||||||
|
{{ __('Issued On') }}:
|
||||||
|
{{ dayjs(certificate.data.issue_date).format('DD MMM YYYY') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<UpcomingEvaluations v-if="courses.length" :courses="courses" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
import { Breadcrumbs, call, createResource } from 'frappe-ui'
|
||||||
|
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||||
|
|
||||||
|
const courseTitle = ref(null)
|
||||||
|
const evaluator = ref(null)
|
||||||
|
const courses = ref([])
|
||||||
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
courseName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCourseDetails()
|
||||||
|
})
|
||||||
|
|
||||||
|
const certificate = createResource({
|
||||||
|
url: 'frappe.client.get_value',
|
||||||
|
params: {
|
||||||
|
doctype: 'LMS Certificate',
|
||||||
|
filters: {
|
||||||
|
member: user.data?.name,
|
||||||
|
course: props.courseName,
|
||||||
|
},
|
||||||
|
fieldname: ['name', 'template', 'issue_date'],
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCourseDetails = () => {
|
||||||
|
call('frappe.client.get_value', {
|
||||||
|
doctype: 'LMS Course',
|
||||||
|
filters: { name: props.courseName },
|
||||||
|
fieldname: ['title', 'evaluator'],
|
||||||
|
}).then((data) => {
|
||||||
|
courseTitle.value = data.title
|
||||||
|
evaluator.value = data.evaluator
|
||||||
|
populateCourses()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const populateCourses = () => {
|
||||||
|
courses.value = [
|
||||||
|
{
|
||||||
|
course: props.courseName,
|
||||||
|
title: courseTitle.value,
|
||||||
|
evaluator: evaluator.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCertificate = (certificate) => {
|
||||||
|
window.open(
|
||||||
|
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||||
|
certificate.name
|
||||||
|
}&format=${encodeURIComponent(certificate.template)}`,
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => [
|
||||||
|
{
|
||||||
|
label: __('Courses'),
|
||||||
|
route: { name: 'Courses' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: courseTitle.value,
|
||||||
|
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Certification'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -196,11 +196,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container border-t">
|
<div class="container border-t space-y-4">
|
||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5">
|
||||||
{{ __('Pricing and Certification') }}
|
{{ __('Pricing and Certification') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 mb-4">
|
<div class="grid grid-cols-3">
|
||||||
<FormControl
|
<FormControl
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="course.paid_course"
|
v-model="course.paid_course"
|
||||||
@@ -217,17 +217,19 @@
|
|||||||
:label="__('Paid Certificate')"
|
:label="__('Paid Certificate')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl v-model="course.course_price" :label="__('Amount')" />
|
||||||
v-model="course.course_price"
|
|
||||||
:label="__('Amount')"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
<Link
|
<Link
|
||||||
doctype="Currency"
|
doctype="Currency"
|
||||||
v-model="course.currency"
|
v-model="course.currency"
|
||||||
:filters="{ enabled: 1 }"
|
:filters="{ enabled: 1 }"
|
||||||
:label="__('Currency')"
|
:label="__('Currency')"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="course.paid_certificate"
|
||||||
|
doctype="Course Evaluator"
|
||||||
|
v-model="course.evaluator"
|
||||||
|
:label="__('Evaluator')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,6 +301,7 @@ const course = reactive({
|
|||||||
disable_self_learning: false,
|
disable_self_learning: false,
|
||||||
enable_certification: false,
|
enable_certification: false,
|
||||||
paid_course: false,
|
paid_course: false,
|
||||||
|
paid_certificate: false,
|
||||||
course_price: '',
|
course_price: '',
|
||||||
currency: '',
|
currency: '',
|
||||||
})
|
})
|
||||||
@@ -394,6 +397,7 @@ const courseResource = createResource({
|
|||||||
'paid_course',
|
'paid_course',
|
||||||
'featured',
|
'featured',
|
||||||
'enable_certification',
|
'enable_certification',
|
||||||
|
'paid_certifiate',
|
||||||
]
|
]
|
||||||
for (let idx in checkboxes) {
|
for (let idx in checkboxes) {
|
||||||
let key = checkboxes[idx]
|
let key = checkboxes[idx]
|
||||||
|
|||||||
@@ -4,45 +4,7 @@
|
|||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
<div
|
<CertificationLinks :courseName="courseName" />
|
||||||
v-if="
|
|
||||||
lesson.data && lesson.data.membership && lesson.data.paid_certificate
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-if="!lesson.data.membership.purchased_certificate"
|
|
||||||
:to="{
|
|
||||||
name: 'Billing',
|
|
||||||
params: {
|
|
||||||
type: 'certificate',
|
|
||||||
name: courseName,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Button>
|
|
||||||
<template #prefix>
|
|
||||||
<GraduationCap class="size-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
{{ __('Get Certified') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
v-else-if="!lesson.data.membership.certficate"
|
|
||||||
:to="{
|
|
||||||
name: 'CourseCertification',
|
|
||||||
params: {
|
|
||||||
courseName: courseName,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Button>
|
|
||||||
<template #prefix>
|
|
||||||
<GraduationCap class="size-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
{{ __('Get Certified') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
||||||
<div
|
<div
|
||||||
@@ -243,6 +205,7 @@ import EditorJS from '@editorjs/editorjs'
|
|||||||
import LessonContent from '@/components/LessonContent.vue'
|
import LessonContent from '@/components/LessonContent.vue'
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import ProgressBar from '@/components/ProgressBar.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
import CertificationLinks from '@/components/CertificationLinks.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -1278,3 +1278,21 @@ def cancel_evaluation(evaluation):
|
|||||||
|
|
||||||
frappe.delete_doc("Event Participants", event.name, ignore_permissions=True)
|
frappe.delete_doc("Event Participants", event.name, ignore_permissions=True)
|
||||||
frappe.delete_doc("Event", event.parent, ignore_permissions=True)
|
frappe.delete_doc("Event", event.parent, ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_certification_details(course):
|
||||||
|
membership = None
|
||||||
|
filters = {"course": course, "member": frappe.session.user}
|
||||||
|
|
||||||
|
if frappe.db.exists("LMS Enrollment", filters):
|
||||||
|
membership = frappe.db.get_value(
|
||||||
|
"LMS Enrollment",
|
||||||
|
filters,
|
||||||
|
["name", "certificate", "purchased_certificate"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
paid_certificate = frappe.db.get_value("LMS Course", course, "paid_certificate")
|
||||||
|
|
||||||
|
return {"membership": membership, "paid_certificate": paid_certificate}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-15 18:45:08.614466",
|
"modified": "2025-02-24 12:17:08.436659",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Course Evaluator",
|
"name": "Course Evaluator",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class CourseEvaluator(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_schedule(course, date, batch=None):
|
def get_schedule(course, date, batch=None):
|
||||||
|
print(batch)
|
||||||
evaluator = get_evaluator(course, batch)
|
evaluator = get_evaluator(course, batch)
|
||||||
day = datetime.strptime(date, "%Y-%m-%d").strftime("%A")
|
day = datetime.strptime(date, "%Y-%m-%d").strftime("%A")
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"paid_course",
|
"paid_course",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
"paid_certificate",
|
"paid_certificate",
|
||||||
|
"evaluator",
|
||||||
"column_break_acoj",
|
"column_break_acoj",
|
||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
@@ -264,6 +265,13 @@
|
|||||||
"fieldname": "paid_certificate",
|
"fieldname": "paid_certificate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Paid Certificate"
|
"label": "Paid Certificate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "paid_certificate",
|
||||||
|
"fieldname": "evaluator",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Evaluator",
|
||||||
|
"options": "Course Evaluator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -290,7 +298,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2025-02-20 16:44:38.891383",
|
"modified": "2025-02-24 11:50:58.325804",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class LMSCourse(Document):
|
|||||||
self.validate_video_link()
|
self.validate_video_link()
|
||||||
self.validate_status()
|
self.validate_status()
|
||||||
self.validate_payments_app()
|
self.validate_payments_app()
|
||||||
|
self.validate_evaluator()
|
||||||
self.validate_amount_and_currency()
|
self.validate_amount_and_currency()
|
||||||
self.image = validate_image(self.image)
|
self.image = validate_image(self.image)
|
||||||
|
|
||||||
@@ -51,6 +52,10 @@ class LMSCourse(Document):
|
|||||||
if "payments" not in installed_apps:
|
if "payments" not in installed_apps:
|
||||||
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
||||||
|
|
||||||
|
def validate_evaluator(self):
|
||||||
|
if self.paid_certificate and not self.evaluator:
|
||||||
|
frappe.throw(_("Evaluator is required for paid certificates."))
|
||||||
|
|
||||||
def validate_amount_and_currency(self):
|
def validate_amount_and_currency(self):
|
||||||
if self.paid_course and (cint(self.course_price) < 0 or not self.currency):
|
if self.paid_course and (cint(self.course_price) < 0 or not self.currency):
|
||||||
frappe.throw(_("Amount and currency are required for paid courses."))
|
frappe.throw(_("Amount and currency are required for paid courses."))
|
||||||
|
|||||||
@@ -855,13 +855,16 @@ def is_onboarding_complete():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_evaluator(course, batch):
|
def get_evaluator(course, batch=None):
|
||||||
evaluator = None
|
evaluator = None
|
||||||
evaluator = frappe.db.get_value(
|
if batch:
|
||||||
"Batch Course",
|
evaluator = frappe.db.get_value(
|
||||||
{"parent": batch, "course": course},
|
"Batch Course",
|
||||||
"evaluator",
|
{"parent": batch, "course": course},
|
||||||
)
|
"evaluator",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
||||||
return evaluator
|
return evaluator
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user