mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge pull request #1263 from FahidLatheef/feat/scorm-progress
feat: SCORM Course Resume Functionality
This commit is contained in:
@@ -12,7 +12,10 @@
|
||||
user.data?.is_instructor)
|
||||
"
|
||||
>
|
||||
<iframe :src="chapter.doc.launch_file" class="w-full h-screen" />
|
||||
<iframe
|
||||
:src="chapter.doc.launch_file"
|
||||
class="w-full h-[calc(100vh-3.00rem)]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!enrollment.data?.length">
|
||||
<div class="text-center pt-10 px-5 md:px-0 pb-10">
|
||||
@@ -49,6 +52,12 @@ const { brand } = sessionStore()
|
||||
const sidebarStore = useSidebar()
|
||||
const user = inject('$user')
|
||||
const readyToRender = ref(false)
|
||||
const isSuccessfullyCompleted = ref(false)
|
||||
|
||||
// If courseRestartOnFailure is true, student has to restart the whole course if failed.
|
||||
// Otherwise, student could retake the final quiz portion.
|
||||
// Ideally, this should be configurable along with `Number of failures before course should restart`.
|
||||
const courseRestartOnFailure = false
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
@@ -88,25 +97,51 @@ const enrollment = createListResource({
|
||||
})
|
||||
|
||||
const getDataFromLMS = (key) => {
|
||||
if (key == 'cmi.core.lesson_status') {
|
||||
if (progress.data?.status == 'Complete') {
|
||||
return 'passed'
|
||||
}
|
||||
return 'incomplete'
|
||||
if (key === 'cmi.core.lesson_status') {
|
||||
return progress.data?.status === 'Complete' ? 'passed' : 'incomplete'
|
||||
} else if (key === 'cmi.launch_data') {
|
||||
return progress.data?.scorm_content || ''
|
||||
} else if (key === 'cmi.suspend_data') {
|
||||
return progress.data?.scorm_content || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
let saveTimeout = null
|
||||
const debouncedSaveProgress = (scormDetails) => {
|
||||
clearTimeout(saveTimeout)
|
||||
saveTimeout = setTimeout(() => {
|
||||
saveProgress(scormDetails)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const saveDataToLMS = (key, value) => {
|
||||
if (key == 'cmi.core.lesson_status' && value == 'passed') {
|
||||
saveProgress()
|
||||
if (key === 'cmi.core.lesson_status') {
|
||||
if (value === 'passed') {
|
||||
isSuccessfullyCompleted.value = true
|
||||
saveProgress({
|
||||
is_complete: isSuccessfullyCompleted.value,
|
||||
scorm_content: '',
|
||||
})
|
||||
} else if (value === 'failed' && courseRestartOnFailure) {
|
||||
saveProgress({
|
||||
is_complete: isSuccessfullyCompleted.value,
|
||||
scorm_content: '',
|
||||
})
|
||||
}
|
||||
} else if (key === 'cmi.suspend_data' && !isSuccessfullyCompleted.value) {
|
||||
debouncedSaveProgress({
|
||||
is_complete: false,
|
||||
scorm_content: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const saveProgress = () => {
|
||||
const saveProgress = (scormDetails = null) => {
|
||||
call('lms.lms.doctype.course_lesson.course_lesson.save_progress', {
|
||||
lesson: chapter.doc.lessons[0].lesson,
|
||||
course: props.courseName,
|
||||
scorm_details: scormDetails,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -115,7 +150,7 @@ const progress = createResource({
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'LMS Course Progress',
|
||||
fieldname: 'status',
|
||||
fieldname: ['status', 'scorm_content'],
|
||||
filters: {
|
||||
member: user.data?.name,
|
||||
lesson: chapter.doc.lessons[0].lesson,
|
||||
|
||||
+1
-1
@@ -1193,7 +1193,7 @@ def fetch_activity_data(member, start_date):
|
||||
lesson_completions = frappe.get_all(
|
||||
"LMS Course Progress",
|
||||
fields=["creation"],
|
||||
filters={"member": member, "creation": [">=", start_date]},
|
||||
filters={"member": member, "creation": [">=", start_date], "status": "Complete"},
|
||||
)
|
||||
|
||||
quiz_submissions = frappe.get_all(
|
||||
|
||||
@@ -46,20 +46,30 @@ class CourseLesson(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_progress(lesson, course):
|
||||
def save_progress(lesson, course, scorm_details=None):
|
||||
"""
|
||||
Note: Pass the argument scorm_details as a dict if it is SCORM related save_progress
|
||||
"""
|
||||
membership = frappe.db.exists("LMS Enrollment", {"course": course, "member": frappe.session.user})
|
||||
if not membership:
|
||||
return 0
|
||||
|
||||
frappe.db.set_value("LMS Enrollment", membership, "current_lesson", lesson)
|
||||
already_completed = frappe.db.exists(
|
||||
progress_already_exists = frappe.db.exists(
|
||||
"LMS Course Progress", {"lesson": lesson, "member": frappe.session.user}
|
||||
)
|
||||
lesson_already_completed = frappe.db.exists(
|
||||
"LMS Course Progress",
|
||||
{"lesson": lesson, "member": frappe.session.user, "status": "Complete"},
|
||||
)
|
||||
|
||||
quiz_completed = get_quiz_progress(lesson)
|
||||
assignment_completed = get_assignment_progress(lesson)
|
||||
|
||||
if not already_completed and quiz_completed and assignment_completed:
|
||||
if scorm_details:
|
||||
scorm_details = frappe._dict(**scorm_details)
|
||||
|
||||
if not progress_already_exists and quiz_completed and assignment_completed and not scorm_details:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Course Progress",
|
||||
@@ -68,6 +78,29 @@ def save_progress(lesson, course):
|
||||
"member": frappe.session.user,
|
||||
}
|
||||
).save(ignore_permissions=True)
|
||||
elif scorm_details and not lesson_already_completed and not progress_already_exists:
|
||||
# Create new SCORM progress
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Course Progress",
|
||||
"lesson": lesson,
|
||||
"status": "Complete" if scorm_details.is_complete else "Partially Complete",
|
||||
"member": frappe.session.user,
|
||||
"scorm_content": "" if scorm_details.is_complete else scorm_details.scorm_content,
|
||||
}
|
||||
).save(ignore_permissions=True)
|
||||
elif scorm_details and not lesson_already_completed and progress_already_exists:
|
||||
# Update Existing SCORM Progress
|
||||
frappe.db.set_value(
|
||||
"LMS Course Progress",
|
||||
progress_already_exists,
|
||||
{
|
||||
"lesson": lesson,
|
||||
"status": "Complete" if scorm_details.is_complete else "Partially Complete",
|
||||
"member": frappe.session.user,
|
||||
"scorm_content": "" if scorm_details.is_complete else scorm_details.scorm_content,
|
||||
},
|
||||
)
|
||||
|
||||
progress = get_course_progress(course)
|
||||
capture_progress_for_analytics(progress, course)
|
||||
|
||||
@@ -270,7 +270,11 @@ def get_timetable_details(timetable):
|
||||
True
|
||||
if frappe.db.exists(
|
||||
"LMS Course Progress",
|
||||
{"lesson": entry.reference_docname, "member": frappe.session.user},
|
||||
{
|
||||
"lesson": entry.reference_docname,
|
||||
"member": frappe.session.user,
|
||||
"status": "Complete",
|
||||
},
|
||||
)
|
||||
else False
|
||||
)
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
"column_break_3",
|
||||
"lesson",
|
||||
"chapter",
|
||||
"course"
|
||||
"course",
|
||||
"section_break_uoob",
|
||||
"is_scorm_chapter",
|
||||
"column_break_wskp",
|
||||
"scorm_content"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -68,11 +72,36 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_uoob",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "chapter.is_scorm_package",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "is_scorm_chapter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is SCORM Chapter",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wskp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_scorm_chapter == 1 && doc.status == 'Partially Complete'",
|
||||
"fieldname": "scorm_content",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "SCORM Content",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-17 15:54:34.040621",
|
||||
"modified": "2025-01-20 22:25:36.829929",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course Progress",
|
||||
@@ -106,4 +135,4 @@
|
||||
"states": [],
|
||||
"title_field": "member_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -325,7 +325,7 @@ def get_progress(course, lesson, member=None):
|
||||
|
||||
return frappe.db.exists(
|
||||
"LMS Course Progress",
|
||||
{"course": course, "member": member, "lesson": lesson},
|
||||
{"course": course, "member": member, "lesson": lesson, "status": "Complete"},
|
||||
["status"],
|
||||
)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% if lesson_completion %} {% set lesson_completion_count =
|
||||
frappe.db.count("LMS Course Progress") %}
|
||||
frappe.db.count("LMS Course Progress", { "status": "Complete" }) %}
|
||||
<div class="common-card-style p-4 flex-column">
|
||||
<div class="stats-label">{{ _("Lessons Completed") }}</div>
|
||||
<div class="stats-value">
|
||||
|
||||
Reference in New Issue
Block a user