feat: generate certificate from course page
This commit is contained in:
@@ -63,7 +63,13 @@
|
||||
{{ __('Start Learning') }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button v-if="canGetCertificate">
|
||||
<Button
|
||||
v-if="canGetCertificate"
|
||||
@click="fetchCertificate()"
|
||||
variant="subtle"
|
||||
class="w-full mt-2"
|
||||
size="md"
|
||||
>
|
||||
{{ __('Get Certificate') }}
|
||||
</Button>
|
||||
<router-link
|
||||
@@ -139,7 +145,7 @@ function enrollStudent() {
|
||||
})
|
||||
setTimeout(() => {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
}, 3000)
|
||||
}, 2000)
|
||||
} else {
|
||||
const enrollStudentResource = createResource({
|
||||
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
|
||||
@@ -179,6 +185,37 @@ const is_instructor = () => {
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<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" />
|
||||
</video>
|
||||
<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">
|
||||
<template #icon>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
{{ lesson.data.course_title }}
|
||||
</div>
|
||||
<div v-if="user && lesson.data.membership" class="text-sm mt-3">
|
||||
{{ Math.ceil(lessonProgress) }}% completed
|
||||
{{ Math.ceil(lessonProgress) }}% {{ __('completed') }}
|
||||
</div>
|
||||
|
||||
<ProgressBar
|
||||
@@ -190,7 +190,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
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 UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
@@ -208,6 +208,8 @@ const allowDiscussions = ref(false)
|
||||
const editor = ref(null)
|
||||
const instructorEditor = ref(null)
|
||||
const lessonProgress = ref(0)
|
||||
const timer = ref(0)
|
||||
let timerInterval
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
@@ -224,6 +226,10 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
startTimer()
|
||||
})
|
||||
|
||||
const lesson = createResource({
|
||||
url: 'lms.lms.utils.get_lesson',
|
||||
cache: ['lesson', props.courseName, props.chapterNumber, props.lessonNumber],
|
||||
@@ -237,7 +243,6 @@ const lesson = createResource({
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
lessonProgress.value = data.membership?.progress
|
||||
markProgress(data)
|
||||
if (data.content) editor.value = renderEditor('editor', data.content)
|
||||
if (data.instructor_content?.blocks?.length)
|
||||
instructorEditor.value = renderEditor(
|
||||
@@ -269,11 +274,9 @@ const renderEditor = (holder, content) => {
|
||||
})
|
||||
}
|
||||
|
||||
const markProgress = (data) => {
|
||||
if (user.data && !data.progress) {
|
||||
setTimeout(() => {
|
||||
progress.submit()
|
||||
}, 30000)
|
||||
const markProgress = () => {
|
||||
if (user.data && !lesson.data?.progress) {
|
||||
progress.submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,10 +328,32 @@ watch(
|
||||
chapter: newChapterNumber,
|
||||
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 = () => {
|
||||
let quizPresent = false
|
||||
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
|
||||
|
||||
47
lms/hooks.py
47
lms/hooks.py
@@ -176,52 +176,7 @@ update_website_context = [
|
||||
]
|
||||
|
||||
jinja = {
|
||||
"methods": [
|
||||
"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",
|
||||
],
|
||||
"methods": ["lms.lms.utils.get_signup_optin_checks"],
|
||||
"filters": [],
|
||||
}
|
||||
## 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}
|
||||
)
|
||||
if not membership:
|
||||
return 0
|
||||
return
|
||||
|
||||
frappe.db.set_value("LMS Enrollment", membership, "current_lesson", lesson)
|
||||
|
||||
@@ -104,7 +104,7 @@ def save_progress(lesson, course):
|
||||
if frappe.db.exists(
|
||||
"LMS Course Progress", {"lesson": lesson, "member": frappe.session.user}
|
||||
):
|
||||
return 0
|
||||
return
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
@@ -116,9 +116,14 @@ def save_progress(lesson, course):
|
||||
).save(ignore_permissions=True)
|
||||
|
||||
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.progress = progress
|
||||
enrollment.save()
|
||||
enrollment.run_method("on_change")
|
||||
|
||||
print("Progress", progress)
|
||||
return progress
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
"label": "Comments"
|
||||
},
|
||||
{
|
||||
"fetch_from": "course.evaluator",
|
||||
"fieldname": "evaluator",
|
||||
"fieldtype": "Link",
|
||||
"label": "Evaluator",
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
"field_order": [
|
||||
"course",
|
||||
"course_title",
|
||||
"column_break_3",
|
||||
"member",
|
||||
"member_name",
|
||||
"column_break_3",
|
||||
"published",
|
||||
"section_break_tnnm",
|
||||
"template",
|
||||
"issue_date",
|
||||
"expiry_date",
|
||||
"batch_name",
|
||||
"published"
|
||||
"column_break_qtzo",
|
||||
"issue_date",
|
||||
"expiry_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -85,11 +87,19 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Course Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_tnnm",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qtzo",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-21 18:14:30.491841",
|
||||
"modified": "2024-07-12 12:39:50.076937",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
@@ -120,13 +130,15 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -71,8 +71,11 @@ class LMSCertificate(Document):
|
||||
|
||||
|
||||
def has_website_permission(doc, ptype, user, verbose=False):
|
||||
print(doc.member, user, ptype)
|
||||
if ptype in ["read", "print"]:
|
||||
return True
|
||||
if doc.member == user and ptype == "create":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -81,7 +84,9 @@ def create_certificate(course):
|
||||
certificate = is_certified(course)
|
||||
|
||||
if certificate:
|
||||
return certificate
|
||||
return frappe.db.get_value(
|
||||
"LMS Certificate", certificate, ["name", "course", "template"], as_dict=True
|
||||
)
|
||||
|
||||
else:
|
||||
expires_after_yrs = int(frappe.db.get_value("LMS Course", course, "expiry"))
|
||||
|
||||
@@ -30,23 +30,23 @@
|
||||
"disable_self_learning",
|
||||
"section_break_18",
|
||||
"short_introduction",
|
||||
"column_break_viqw",
|
||||
"description",
|
||||
"section_break_gglp",
|
||||
"chapters",
|
||||
"related_courses",
|
||||
"pricing_tab",
|
||||
"pricing_section",
|
||||
"paid_course",
|
||||
"column_break_acoj",
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"certification_tab",
|
||||
"certification_section",
|
||||
"enable_certification",
|
||||
"expiry",
|
||||
"max_attempts",
|
||||
"column_break_rxww",
|
||||
"grant_certificate_after",
|
||||
"evaluator",
|
||||
"duration"
|
||||
"expiry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -129,8 +129,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "certification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Certification"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -170,25 +169,9 @@
|
||||
"fieldname": "column_break_10",
|
||||
"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",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Pricing"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_course",
|
||||
@@ -198,20 +181,6 @@
|
||||
"mandatory_depends_on": "paid_course",
|
||||
"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",
|
||||
"fieldname": "paid_course",
|
||||
@@ -250,6 +219,24 @@
|
||||
"fieldname": "featured",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
@@ -276,7 +263,7 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-06-24 17:44:45.903164",
|
||||
"modified": "2024-07-12 13:54:40.474097",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
|
||||
from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
|
||||
|
||||
from .utils import get_evaluation_details, slugify
|
||||
from .utils import slugify
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
@@ -20,58 +16,3 @@ class TestUtils(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
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]
|
||||
|
||||
|
||||
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):
|
||||
amount_reduced = amount / 1000
|
||||
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):
|
||||
if path and "/private" in 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
|
||||
|
||||
|
||||
def get_evaluator(course, batch=None):
|
||||
def get_evaluator(course, batch):
|
||||
evaluator = None
|
||||
|
||||
if batch:
|
||||
evaluator = frappe.db.get_value(
|
||||
"Batch Course",
|
||||
{"parent": batch, "course": course},
|
||||
"evaluator",
|
||||
)
|
||||
|
||||
if not evaluator:
|
||||
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
||||
|
||||
evaluator = frappe.db.get_value(
|
||||
"Batch Course",
|
||||
{"parent": batch, "course": course},
|
||||
"evaluator",
|
||||
)
|
||||
return evaluator
|
||||
|
||||
|
||||
@@ -1285,6 +1232,7 @@ def get_course_details(course):
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"enable_certification",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user