feat: generate certificate from course page
This commit is contained in:
@@ -63,7 +63,13 @@
|
|||||||
{{ __('Start Learning') }}
|
{{ __('Start Learning') }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="canGetCertificate">
|
<Button
|
||||||
|
v-if="canGetCertificate"
|
||||||
|
@click="fetchCertificate()"
|
||||||
|
variant="subtle"
|
||||||
|
class="w-full mt-2"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
{{ __('Get Certificate') }}
|
{{ __('Get Certificate') }}
|
||||||
</Button>
|
</Button>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -139,7 +145,7 @@ function enrollStudent() {
|
|||||||
})
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||||
}, 3000)
|
}, 2000)
|
||||||
} else {
|
} else {
|
||||||
const enrollStudentResource = createResource({
|
const enrollStudentResource = createResource({
|
||||||
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
|
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
|
||||||
@@ -179,6 +185,37 @@ const is_instructor = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canGetCertificate = computed(() => {
|
const canGetCertificate = computed(() => {
|
||||||
console.log(props.course)
|
if (
|
||||||
|
props.course.data?.enable_certification &&
|
||||||
|
props.course.data?.membership?.progress == 100
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const certificate = createResource({
|
||||||
|
url: 'lms.lms.doctype.lms_certificate.lms_certificate.create_certificate',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
course: values.course,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
console.log(data)
|
||||||
|
window.open(
|
||||||
|
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
|
||||||
|
data.name
|
||||||
|
}&format=${encodeURIComponent(data.template)}`,
|
||||||
|
'_blank'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchCertificate = () => {
|
||||||
|
certificate.submit({
|
||||||
|
course: props.course.data?.name,
|
||||||
|
member: user.data?.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="videoContainer" class="video-block group relative">
|
<div ref="videoContainer" class="video-block group relative">
|
||||||
<video @timeupdate="updateTime" @ended="videoEnded" class="rounded-lg">
|
<video
|
||||||
|
@timeupdate="updateTime"
|
||||||
|
@ended="videoEnded"
|
||||||
|
class="rounded-lg border border-gray-100"
|
||||||
|
>
|
||||||
<source :src="fileURL" :type="type" />
|
<source :src="fileURL" :type="type" />
|
||||||
</video>
|
</video>
|
||||||
<div
|
<div
|
||||||
class="flex items-center space-x-2 bg-gray-200 rounded-lg p-0.5 absolute bottom-3 w-[98%] left-0 right-0 mx-auto"
|
class="flex items-center space-x-2 bg-gray-200 rounded-md p-0.5 absolute bottom-3 w-[98%] left-0 right-0 mx-auto"
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
{{ lesson.data.course_title }}
|
{{ lesson.data.course_title }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="user && lesson.data.membership" class="text-sm mt-3">
|
<div v-if="user && lesson.data.membership" class="text-sm mt-3">
|
||||||
{{ Math.ceil(lessonProgress) }}% completed
|
{{ Math.ceil(lessonProgress) }}% {{ __('completed') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Breadcrumbs, Button } from 'frappe-ui'
|
import { createResource, Breadcrumbs, Button } from 'frappe-ui'
|
||||||
import { computed, watch, inject, ref } from 'vue'
|
import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@@ -208,6 +208,8 @@ const allowDiscussions = ref(false)
|
|||||||
const editor = ref(null)
|
const editor = ref(null)
|
||||||
const instructorEditor = ref(null)
|
const instructorEditor = ref(null)
|
||||||
const lessonProgress = ref(0)
|
const lessonProgress = ref(0)
|
||||||
|
const timer = ref(0)
|
||||||
|
let timerInterval
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
@@ -224,6 +226,10 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startTimer()
|
||||||
|
})
|
||||||
|
|
||||||
const lesson = createResource({
|
const lesson = createResource({
|
||||||
url: 'lms.lms.utils.get_lesson',
|
url: 'lms.lms.utils.get_lesson',
|
||||||
cache: ['lesson', props.courseName, props.chapterNumber, props.lessonNumber],
|
cache: ['lesson', props.courseName, props.chapterNumber, props.lessonNumber],
|
||||||
@@ -237,7 +243,6 @@ const lesson = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
lessonProgress.value = data.membership?.progress
|
lessonProgress.value = data.membership?.progress
|
||||||
markProgress(data)
|
|
||||||
if (data.content) editor.value = renderEditor('editor', data.content)
|
if (data.content) editor.value = renderEditor('editor', data.content)
|
||||||
if (data.instructor_content?.blocks?.length)
|
if (data.instructor_content?.blocks?.length)
|
||||||
instructorEditor.value = renderEditor(
|
instructorEditor.value = renderEditor(
|
||||||
@@ -269,11 +274,9 @@ const renderEditor = (holder, content) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const markProgress = (data) => {
|
const markProgress = () => {
|
||||||
if (user.data && !data.progress) {
|
if (user.data && !lesson.data?.progress) {
|
||||||
setTimeout(() => {
|
progress.submit()
|
||||||
progress.submit()
|
|
||||||
}, 30000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,10 +328,32 @@ watch(
|
|||||||
chapter: newChapterNumber,
|
chapter: newChapterNumber,
|
||||||
lesson: newLessonNumber,
|
lesson: newLessonNumber,
|
||||||
})
|
})
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
timer.value = 0
|
||||||
|
startTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
console.log('starting timer')
|
||||||
|
timerInterval = setInterval(() => {
|
||||||
|
timer.value++
|
||||||
|
console.log(timer.value)
|
||||||
|
if (timer.value == 30) {
|
||||||
|
console.log('30 seconds passed')
|
||||||
|
console.log(lesson.data?.title)
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
markProgress()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
console.log('clearing interval')
|
||||||
|
clearInterval(timerInterval)
|
||||||
|
})
|
||||||
|
|
||||||
const checkIfDiscussionsAllowed = () => {
|
const checkIfDiscussionsAllowed = () => {
|
||||||
let quizPresent = false
|
let quizPresent = false
|
||||||
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
|
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
|
||||||
|
|||||||
47
lms/hooks.py
47
lms/hooks.py
@@ -176,52 +176,7 @@ update_website_context = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
jinja = {
|
jinja = {
|
||||||
"methods": [
|
"methods": ["lms.lms.utils.get_signup_optin_checks"],
|
||||||
"lms.page_renderers.get_profile_url",
|
|
||||||
"lms.overrides.user.get_enrolled_courses",
|
|
||||||
"lms.overrides.user.get_course_membership",
|
|
||||||
"lms.overrides.user.get_authored_courses",
|
|
||||||
"lms.overrides.user.get_palette",
|
|
||||||
"lms.lms.utils.get_membership",
|
|
||||||
"lms.lms.utils.get_lessons",
|
|
||||||
"lms.lms.utils.get_tags",
|
|
||||||
"lms.lms.utils.get_instructors",
|
|
||||||
"lms.lms.utils.get_students",
|
|
||||||
"lms.lms.utils.get_average_rating",
|
|
||||||
"lms.lms.utils.is_certified",
|
|
||||||
"lms.lms.utils.get_lesson_index",
|
|
||||||
"lms.lms.utils.get_lesson_url",
|
|
||||||
"lms.lms.utils.get_chapters",
|
|
||||||
"lms.lms.utils.get_slugified_chapter_title",
|
|
||||||
"lms.lms.utils.get_progress",
|
|
||||||
"lms.lms.utils.render_html",
|
|
||||||
"lms.lms.utils.is_mentor",
|
|
||||||
"lms.lms.utils.is_cohort_staff",
|
|
||||||
"lms.lms.utils.get_mentors",
|
|
||||||
"lms.lms.utils.get_reviews",
|
|
||||||
"lms.lms.utils.is_eligible_to_review",
|
|
||||||
"lms.lms.utils.get_initial_members",
|
|
||||||
"lms.lms.utils.get_sorted_reviews",
|
|
||||||
"lms.lms.utils.is_instructor",
|
|
||||||
"lms.lms.utils.convert_number_to_character",
|
|
||||||
"lms.lms.utils.get_signup_optin_checks",
|
|
||||||
"lms.lms.utils.get_popular_courses",
|
|
||||||
"lms.lms.utils.format_amount",
|
|
||||||
"lms.lms.utils.first_lesson_exists",
|
|
||||||
"lms.lms.utils.get_courses_under_review",
|
|
||||||
"lms.lms.utils.has_course_instructor_role",
|
|
||||||
"lms.lms.utils.has_course_moderator_role",
|
|
||||||
"lms.lms.utils.get_certificates",
|
|
||||||
"lms.lms.utils.format_number",
|
|
||||||
"lms.lms.utils.get_lesson_count",
|
|
||||||
"lms.lms.utils.get_all_memberships",
|
|
||||||
"lms.lms.utils.get_filtered_membership",
|
|
||||||
"lms.lms.utils.show_start_learing_cta",
|
|
||||||
"lms.lms.utils.can_create_courses",
|
|
||||||
"lms.lms.utils.get_telemetry_boot_info",
|
|
||||||
"lms.lms.utils.is_onboarding_complete",
|
|
||||||
"lms.www.utils.is_student",
|
|
||||||
],
|
|
||||||
"filters": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
## Specify the additional tabs to be included in the user profile page.
|
## Specify the additional tabs to be included in the user profile page.
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def save_progress(lesson, course):
|
|||||||
"LMS Enrollment", {"course": course, "member": frappe.session.user}
|
"LMS Enrollment", {"course": course, "member": frappe.session.user}
|
||||||
)
|
)
|
||||||
if not membership:
|
if not membership:
|
||||||
return 0
|
return
|
||||||
|
|
||||||
frappe.db.set_value("LMS Enrollment", membership, "current_lesson", lesson)
|
frappe.db.set_value("LMS Enrollment", membership, "current_lesson", lesson)
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ def save_progress(lesson, course):
|
|||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
"LMS Course Progress", {"lesson": lesson, "member": frappe.session.user}
|
"LMS Course Progress", {"lesson": lesson, "member": frappe.session.user}
|
||||||
):
|
):
|
||||||
return 0
|
return
|
||||||
|
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -116,9 +116,14 @@ def save_progress(lesson, course):
|
|||||||
).save(ignore_permissions=True)
|
).save(ignore_permissions=True)
|
||||||
|
|
||||||
progress = get_course_progress(course)
|
progress = get_course_progress(course)
|
||||||
frappe.db.set_value("LMS Enrollment", membership, "progress", progress)
|
print("Progress", progress)
|
||||||
|
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necesary for badge to get assigned.
|
||||||
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
||||||
|
enrollment.progress = progress
|
||||||
|
enrollment.save()
|
||||||
enrollment.run_method("on_change")
|
enrollment.run_method("on_change")
|
||||||
|
|
||||||
|
print("Progress", progress)
|
||||||
return progress
|
return progress
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@
|
|||||||
"label": "Comments"
|
"label": "Comments"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "course.evaluator",
|
|
||||||
"fieldname": "evaluator",
|
"fieldname": "evaluator",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Evaluator",
|
"label": "Evaluator",
|
||||||
|
|||||||
@@ -8,14 +8,16 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"course",
|
"course",
|
||||||
"course_title",
|
"course_title",
|
||||||
|
"column_break_3",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"column_break_3",
|
"published",
|
||||||
|
"section_break_tnnm",
|
||||||
"template",
|
"template",
|
||||||
"issue_date",
|
|
||||||
"expiry_date",
|
|
||||||
"batch_name",
|
"batch_name",
|
||||||
"published"
|
"column_break_qtzo",
|
||||||
|
"issue_date",
|
||||||
|
"expiry_date"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -85,11 +87,19 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Course Title",
|
"label": "Course Title",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_tnnm",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_qtzo",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-21 18:14:30.491841",
|
"modified": "2024-07-12 12:39:50.076937",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
@@ -120,13 +130,15 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "LMS Student",
|
"role": "LMS Student",
|
||||||
"share": 1
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -71,8 +71,11 @@ class LMSCertificate(Document):
|
|||||||
|
|
||||||
|
|
||||||
def has_website_permission(doc, ptype, user, verbose=False):
|
def has_website_permission(doc, ptype, user, verbose=False):
|
||||||
|
print(doc.member, user, ptype)
|
||||||
if ptype in ["read", "print"]:
|
if ptype in ["read", "print"]:
|
||||||
return True
|
return True
|
||||||
|
if doc.member == user and ptype == "create":
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +84,9 @@ def create_certificate(course):
|
|||||||
certificate = is_certified(course)
|
certificate = is_certified(course)
|
||||||
|
|
||||||
if certificate:
|
if certificate:
|
||||||
return certificate
|
return frappe.db.get_value(
|
||||||
|
"LMS Certificate", certificate, ["name", "course", "template"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
expires_after_yrs = int(frappe.db.get_value("LMS Course", course, "expiry"))
|
expires_after_yrs = int(frappe.db.get_value("LMS Course", course, "expiry"))
|
||||||
|
|||||||
@@ -30,23 +30,23 @@
|
|||||||
"disable_self_learning",
|
"disable_self_learning",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
"short_introduction",
|
"short_introduction",
|
||||||
|
"column_break_viqw",
|
||||||
"description",
|
"description",
|
||||||
|
"section_break_gglp",
|
||||||
"chapters",
|
"chapters",
|
||||||
"related_courses",
|
"related_courses",
|
||||||
|
"pricing_tab",
|
||||||
"pricing_section",
|
"pricing_section",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
"column_break_acoj",
|
"column_break_acoj",
|
||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
|
"certification_tab",
|
||||||
"certification_section",
|
"certification_section",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
"expiry",
|
|
||||||
"max_attempts",
|
|
||||||
"column_break_rxww",
|
"column_break_rxww",
|
||||||
"grant_certificate_after",
|
"expiry"
|
||||||
"evaluator",
|
|
||||||
"duration"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -129,8 +129,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "certification_section",
|
"fieldname": "certification_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Certification"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -170,25 +169,9 @@
|
|||||||
"fieldname": "column_break_10",
|
"fieldname": "column_break_10",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "enable_certification",
|
|
||||||
"fieldname": "grant_certificate_after",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Grant Certificate After",
|
|
||||||
"options": "Completion\nEvaluation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
|
|
||||||
"fieldname": "evaluator",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Evaluator",
|
|
||||||
"mandatory_depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
|
|
||||||
"options": "Course Evaluator"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "pricing_section",
|
"fieldname": "pricing_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Pricing"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "paid_course",
|
"depends_on": "paid_course",
|
||||||
@@ -198,20 +181,6 @@
|
|||||||
"mandatory_depends_on": "paid_course",
|
"mandatory_depends_on": "paid_course",
|
||||||
"options": "Currency"
|
"options": "Currency"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
|
|
||||||
"fieldname": "max_attempts",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Max Attempts for Evaluations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
|
|
||||||
"fieldname": "duration",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Duration for Attempts",
|
|
||||||
"options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "paid_course",
|
"fieldname": "paid_course",
|
||||||
@@ -250,6 +219,24 @@
|
|||||||
"fieldname": "featured",
|
"fieldname": "featured",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Featured"
|
"label": "Featured"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_viqw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_gglp",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pricing_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Pricing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certification_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Certification"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -276,7 +263,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-06-24 17:44:45.903164",
|
"modified": "2024-07-12 13:54:40.474097",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import getdate
|
from .utils import slugify
|
||||||
|
|
||||||
from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
|
|
||||||
|
|
||||||
from .utils import get_evaluation_details, slugify
|
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
@@ -20,58 +16,3 @@ class TestUtils(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
slugify("Hello World", ["hello-world", "hello-world-2"]), "hello-world-3"
|
slugify("Hello World", ["hello-world", "hello-world-2"]), "hello-world-3"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_evaluation_details(self):
|
|
||||||
user = new_user("Eval", "eval@test.com")
|
|
||||||
|
|
||||||
course = new_course(
|
|
||||||
"Test Evaluation Details",
|
|
||||||
{
|
|
||||||
"enable_certification": 1,
|
|
||||||
"grant_certificate_after": "Evaluation",
|
|
||||||
"evaluator": "evaluator@example.com",
|
|
||||||
"max_attempts": 3,
|
|
||||||
"duration": 2,
|
|
||||||
"instructors": [{"instructor": user.name}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Two evaluations failed within max attempts. Check eligibility for a third evaluation
|
|
||||||
create_evaluation(user.name, course.name, getdate("21-03-2022"), 0.4, "Fail")
|
|
||||||
create_evaluation(user.name, course.name, getdate("12-04-2022"), 0.4, "Fail")
|
|
||||||
details = get_evaluation_details(course.name, user.name)
|
|
||||||
self.assertTrue(details.eligible)
|
|
||||||
|
|
||||||
# Three evaluations failed within max attempts. Check eligibility for a forth evaluation
|
|
||||||
create_evaluation(user.name, course.name, getdate("21-03-2022"), 0.4, "Fail")
|
|
||||||
create_evaluation(user.name, course.name, getdate("12-04-2022"), 0.4, "Fail")
|
|
||||||
create_evaluation(user.name, course.name, getdate("16-04-2022"), 0.4, "Fail")
|
|
||||||
details = get_evaluation_details(course.name, user.name)
|
|
||||||
self.assertFalse(details.eligible)
|
|
||||||
|
|
||||||
# Three evaluations failed within max attempts. Check eligibility for a forth evaluation. Different Dates
|
|
||||||
create_evaluation(user.name, course.name, getdate("01-03-2022"), 0.4, "Fail")
|
|
||||||
create_evaluation(user.name, course.name, getdate("12-04-2022"), 0.4, "Fail")
|
|
||||||
create_evaluation(user.name, course.name, getdate("16-04-2022"), 0.4, "Fail")
|
|
||||||
details = get_evaluation_details(course.name, user.name)
|
|
||||||
self.assertFalse(details.eligible)
|
|
||||||
|
|
||||||
frappe.db.delete("LMS Certificate Evaluation", {"course": course.name})
|
|
||||||
frappe.db.delete("LMS Course", course.name)
|
|
||||||
frappe.db.delete("User", user.name)
|
|
||||||
|
|
||||||
|
|
||||||
def create_evaluation(user, course, date, rating, status):
|
|
||||||
evaluation = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "LMS Certificate Evaluation",
|
|
||||||
"member": user,
|
|
||||||
"course": course,
|
|
||||||
"date": date,
|
|
||||||
"start_time": "12:00:00",
|
|
||||||
"end_time": "13:00:00",
|
|
||||||
"rating": rating,
|
|
||||||
"status": status,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
evaluation.save()
|
|
||||||
|
|||||||
@@ -452,45 +452,6 @@ def get_popular_courses():
|
|||||||
return course_membership[:3]
|
return course_membership[:3]
|
||||||
|
|
||||||
|
|
||||||
def get_evaluation_details(course, member=None):
|
|
||||||
info = frappe.db.get_value(
|
|
||||||
"LMS Course",
|
|
||||||
course,
|
|
||||||
["grant_certificate_after", "max_attempts", "duration"],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
request = frappe.db.get_value(
|
|
||||||
"LMS Certificate Request",
|
|
||||||
{
|
|
||||||
"course": course,
|
|
||||||
"member": member or frappe.session.user,
|
|
||||||
"date": [">=", getdate()],
|
|
||||||
},
|
|
||||||
["date", "start_time", "end_time"],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
no_of_attempts = frappe.db.count(
|
|
||||||
"LMS Certificate Evaluation",
|
|
||||||
{
|
|
||||||
"course": course,
|
|
||||||
"member": member or frappe.session.user,
|
|
||||||
"status": ["!=", "Pass"],
|
|
||||||
"creation": [">=", add_months(getdate(), -abs(cint(info.duration)))],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return frappe._dict(
|
|
||||||
{
|
|
||||||
"eligible": info.grant_certificate_after == "Evaluation"
|
|
||||||
and not request
|
|
||||||
and no_of_attempts < info.max_attempts,
|
|
||||||
"request": request,
|
|
||||||
"no_of_attempts": no_of_attempts,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_amount(amount, currency):
|
def format_amount(amount, currency):
|
||||||
amount_reduced = amount / 1000
|
amount_reduced = amount / 1000
|
||||||
if amount_reduced < 1:
|
if amount_reduced < 1:
|
||||||
@@ -612,14 +573,6 @@ def get_courses_under_review():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_certificates(member=None):
|
|
||||||
return frappe.get_all(
|
|
||||||
"LMS Certificate",
|
|
||||||
{"member": member or frappe.session.user},
|
|
||||||
["course", "member", "issue_date", "expiry_date", "name"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_image(path):
|
def validate_image(path):
|
||||||
if path and "/private" in path:
|
if path and "/private" in path:
|
||||||
file = frappe.get_doc("File", {"file_url": path})
|
file = frappe.get_doc("File", {"file_url": path})
|
||||||
@@ -944,19 +897,13 @@ def has_graded_assessment(submission):
|
|||||||
return False if status == "Not Graded" else True
|
return False if status == "Not Graded" else True
|
||||||
|
|
||||||
|
|
||||||
def get_evaluator(course, batch=None):
|
def get_evaluator(course, batch):
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not evaluator:
|
|
||||||
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
|
||||||
|
|
||||||
return evaluator
|
return evaluator
|
||||||
|
|
||||||
|
|
||||||
@@ -1285,6 +1232,7 @@ def get_course_details(course):
|
|||||||
"course_price",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"amount_usd",
|
"amount_usd",
|
||||||
|
"enable_certification",
|
||||||
],
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user