diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index fe8b2bd45..727e4ac56 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -119,6 +119,8 @@ declare module 'vue' {
UserDropdown: typeof import('./src/components/Sidebar/UserDropdown.vue')['default']
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
VideoStatistics: typeof import('./src/components/Modals/VideoStatistics.vue')['default']
+ WordHunt: typeof import('./src/components/WordHunt.vue')['default']
+ WordHuntBlock: typeof import('./src/components/WordHuntBlock.vue')['default']
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
ZoomSettings: typeof import('./src/components/Settings/ZoomSettings.vue')['default']
}
diff --git a/frontend/src/components/AssessmentPlugin.vue b/frontend/src/components/AssessmentPlugin.vue
index 382fee2aa..a4256a1ca 100644
--- a/frontend/src/components/AssessmentPlugin.vue
+++ b/frontend/src/components/AssessmentPlugin.vue
@@ -2,12 +2,7 @@
+
diff --git a/frontend/src/components/WordHuntBlock.vue b/frontend/src/components/WordHuntBlock.vue
new file mode 100644
index 000000000..36b94d845
--- /dev/null
+++ b/frontend/src/components/WordHuntBlock.vue
@@ -0,0 +1,31 @@
+
+
+
+
+ {{ __('Please login to access this activity.') }}
+
+
+
+
+
diff --git a/frontend/src/pages/Batches/components/Assessments.vue b/frontend/src/pages/Batches/components/Assessments.vue
index 23ad356f5..3a3580b9b 100644
--- a/frontend/src/pages/Batches/components/Assessments.vue
+++ b/frontend/src/pages/Batches/components/Assessments.vue
@@ -195,6 +195,13 @@ const getRowRoute = (row) => {
activityID: row.assessment_name,
},
}
+ } else if (row.assessment_type == 'LMS Word Hunt Activity') {
+ return {
+ name: 'WordHuntPage',
+ params: {
+ activityID: row.assessment_name,
+ },
+ }
} else {
return {
name: 'QuizPage',
@@ -251,6 +258,8 @@ const getAssessmentTypeLabel = (type) => {
return __('Quiz')
} else if (type == 'LMS Drag Drop Activity') {
return __('Drag & Drop')
+ } else if (type == 'LMS Word Hunt Activity') {
+ return __('Word Hunt')
} else if (type == 'LMS Programming Exercise') {
return __('Programming Exercise')
}
diff --git a/frontend/src/pages/Lesson.vue b/frontend/src/pages/Lesson.vue
index 56b3eaed6..8d5ed110d 100644
--- a/frontend/src/pages/Lesson.vue
+++ b/frontend/src/pages/Lesson.vue
@@ -585,7 +585,9 @@ const checkQuiz = () => {
if (!editor.value && lesson.body) {
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
const dragDropRegex = /\{\{ DragDrop\(".*"\) \}\}/
- hasQuiz.value = quizRegex.test(lesson.body) || dragDropRegex.test(lesson.body)
+ const wordHuntRegex = /\{\{ WordHunt\(".*"\) \}\}/
+ hasQuiz.value =
+ quizRegex.test(lesson.body) || dragDropRegex.test(lesson.body) || wordHuntRegex.test(lesson.body)
if (!hasQuiz.value && !zenModeEnabled) {
allowDiscussions.value = true
} else {
diff --git a/frontend/src/pages/LessonForm.vue b/frontend/src/pages/LessonForm.vue
index cf0c167be..6c1044388 100644
--- a/frontend/src/pages/LessonForm.vue
+++ b/frontend/src/pages/LessonForm.vue
@@ -314,6 +314,14 @@ const convertToJSON = (lessonData) => {
activity: activity,
},
})
+ } else if (block.includes('{{ WordHunt')) {
+ let activity = block.match(/\(["']([^"']+?)["']\)/)[1]
+ blocks.push({
+ type: 'wordHunt',
+ data: {
+ activity: activity,
+ },
+ })
} else if (block.includes('{{ Video')) {
let video = block.match(/\(["']([^"']+?)["']\)/)[1]
blocks.push({
diff --git a/frontend/src/pages/WordHunt/WordHuntActivities.vue b/frontend/src/pages/WordHunt/WordHuntActivities.vue
new file mode 100644
index 000000000..a9eda54ef
--- /dev/null
+++ b/frontend/src/pages/WordHunt/WordHuntActivities.vue
@@ -0,0 +1,321 @@
+
+
+
+
+
+
+
+
+ {{
+ totalActivities.loading
+ ? __('Loading...')
+ : totalActivities.data
+ ? __('{0} Activities').format(totalActivities.data)
+ : __('No Activities')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row[column.key] }}
+
+
+
+
+
+
+ {{ row[column.key] }}
+
+
+
+
+
+
+
+
+ {{ __('No matching activities') }}
+
+
+ {{ __('Try a different title or clear the current search.') }}
+
+
+
+
+
+
+
+
+ {{ pageRangeLabel }}
+
+
+
+
+ {{ __('Page {0} of {1}').format(currentPage, totalPages) }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/WordHunt/WordHuntForm.vue b/frontend/src/pages/WordHunt/WordHuntForm.vue
new file mode 100644
index 000000000..9ff81c709
--- /dev/null
+++ b/frontend/src/pages/WordHunt/WordHuntForm.vue
@@ -0,0 +1,170 @@
+
+
+
+
+
{{ __('Details') }}
+
+
+
+
+
+
+
+
+
+
+
{{ __('Settings') }}
+
+
+
+
+
+
+
+
+
{{ __('Passage') }}
+
+ {{ __('Write the passage naturally and use underscores like _, __, or _______ wherever you want a blank.') }}
+
+
+
+
+
+
+
{{ __('Blanks') }}
+
+
+
+
+
+
+ {{ __('Blank {0}').format(idx + 1) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/WordHunt/WordHuntPage.vue b/frontend/src/pages/WordHunt/WordHuntPage.vue
new file mode 100644
index 000000000..5cf3e79b5
--- /dev/null
+++ b/frontend/src/pages/WordHunt/WordHuntPage.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/WordHunt/WordHuntSubmission.vue b/frontend/src/pages/WordHunt/WordHuntSubmission.vue
new file mode 100644
index 000000000..f2c3aa9b2
--- /dev/null
+++ b/frontend/src/pages/WordHunt/WordHuntSubmission.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
{{ details.doc.member_name }}
+
+
+
+
+ {{ __('Blank') }}:
+ {{ row.idx || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/WordHunt/WordHuntSubmissionList.vue b/frontend/src/pages/WordHunt/WordHuntSubmissionList.vue
new file mode 100644
index 000000000..dab9bc94f
--- /dev/null
+++ b/frontend/src/pages/WordHunt/WordHuntSubmissionList.vue
@@ -0,0 +1,75 @@
+
+
+
+
+ {{ submissions.data[0].activity_title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/router.js b/frontend/src/router.js
index e9448f83f..3b7eeb808 100644
--- a/frontend/src/router.js
+++ b/frontend/src/router.js
@@ -161,6 +161,17 @@ const routes = [
component: () => import('@/pages/DragDrop/DragDropForm.vue'),
props: true,
},
+ {
+ path: '/word-hunt-activities',
+ name: 'WordHuntActivities',
+ component: () => import('@/pages/WordHunt/WordHuntActivities.vue'),
+ },
+ {
+ path: '/word-hunt-activities/:activityID',
+ name: 'WordHuntForm',
+ component: () => import('@/pages/WordHunt/WordHuntForm.vue'),
+ props: true,
+ },
{
path: '/quiz/:quizID',
name: 'QuizPage',
@@ -173,6 +184,12 @@ const routes = [
component: () => import('@/pages/DragDrop/DragDropPage.vue'),
props: true,
},
+ {
+ path: '/word-hunt/:activityID',
+ name: 'WordHuntPage',
+ component: () => import('@/pages/WordHunt/WordHuntPage.vue'),
+ props: true,
+ },
{
path: '/quiz-submissions/:quizID',
name: 'QuizSubmissionList',
@@ -185,6 +202,12 @@ const routes = [
component: () => import('@/pages/DragDrop/DragDropSubmissionList.vue'),
props: true,
},
+ {
+ path: '/word-hunt-submissions/:activityID',
+ name: 'WordHuntSubmissionList',
+ component: () => import('@/pages/WordHunt/WordHuntSubmissionList.vue'),
+ props: true,
+ },
{
path: '/quiz-submission/:submission',
name: 'QuizSubmission',
@@ -197,6 +220,12 @@ const routes = [
component: () => import('@/pages/DragDrop/DragDropSubmission.vue'),
props: true,
},
+ {
+ path: '/word-hunt-submission/:submission',
+ name: 'WordHuntSubmission',
+ component: () => import('@/pages/WordHunt/WordHuntSubmission.vue'),
+ props: true,
+ },
{
path: '/programs',
name: 'Programs',
diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js
index 90101782e..e5858d346 100644
--- a/frontend/src/utils/index.js
+++ b/frontend/src/utils/index.js
@@ -3,6 +3,7 @@ import { useTimeAgo } from '@vueuse/core'
import colorsJSON from '@/utils/frappe-ui-colors.json'
import { Quiz } from '@/utils/quiz'
import { DragDrop } from '@/utils/dragDrop'
+import { WordHunt } from '@/utils/wordHunt'
import { Program } from '@/utils/program'
import { Assignment } from '@/utils/assignment'
import { Upload } from '@/utils/upload'
@@ -134,6 +135,7 @@ export function getEditorTools() {
},
quiz: Quiz,
dragDrop: DragDrop,
+ wordHunt: WordHunt,
assignment: Assignment,
program: Program,
markdown: {
@@ -558,6 +560,21 @@ const getSidebarItems = () => {
'DragDropSubmission',
],
},
+ {
+ label: 'Word Hunt Activities',
+ icon: 'Search',
+ to: 'WordHuntActivities',
+ condition: () => {
+ return isAdmin()
+ },
+ activeFor: [
+ 'WordHuntActivities',
+ 'WordHuntForm',
+ 'WordHuntPage',
+ 'WordHuntSubmissionList',
+ 'WordHuntSubmission',
+ ],
+ },
{
label: 'Assignments',
icon: 'Pencil',
diff --git a/frontend/src/utils/wordHunt.js b/frontend/src/utils/wordHunt.js
new file mode 100644
index 000000000..668182d59
--- /dev/null
+++ b/frontend/src/utils/wordHunt.js
@@ -0,0 +1,115 @@
+import AssessmentPlugin from '@/components/AssessmentPlugin.vue'
+import { createApp, h } from 'vue'
+import translationPlugin from '../translation'
+import { Search } from 'lucide-vue-next'
+import { getLmsRoute } from '@/utils/basePath'
+
+export class WordHunt {
+ constructor({ data, readOnly }) {
+ this.data = data
+ this.readOnly = readOnly
+ }
+
+ static get toolbox() {
+ const app = createApp({
+ render: () => h(Search, { size: 18, strokeWidth: 1.5 }),
+ })
+
+ const div = document.createElement('div')
+ app.mount(div)
+
+ return {
+ title: __('Word Hunt'),
+ icon: div.innerHTML,
+ }
+ }
+
+ static get isReadOnlySupported() {
+ return true
+ }
+
+ render() {
+ this.wrapper = document.createElement('div')
+ if (Object.keys(this.data).length) {
+ this.renderActivity(this.data.activity)
+ } else {
+ this.renderActivityModal()
+ }
+ return this.wrapper
+ }
+
+ renderActivity(activity) {
+ if (this.readOnly) {
+ const activityPath = getLmsRoute(`word-hunt/${activity}?fromLesson=1`)
+
+ const iframe = document.createElement('iframe')
+ iframe.src = activityPath
+ iframe.className = 'w-full'
+ iframe.style.border = 'none'
+ iframe.style.height = '200px'
+
+ iframe.addEventListener('load', () => {
+ let lastHeight = 0
+ let attempts = 0
+ const poll = setInterval(() => {
+ try {
+ const height = iframe.contentWindow.document.body.scrollHeight
+ if (height === lastHeight || attempts > 20) {
+ clearInterval(poll)
+ if (height > 0) iframe.style.height = height + 'px'
+ }
+ lastHeight = height
+ attempts++
+ } catch (e) {
+ clearInterval(poll)
+ }
+ }, 100)
+ })
+
+ this.wrapper.appendChild(iframe)
+
+ setTimeout(() => {
+ iframe.style.height = iframe.contentWindow.document.body.scrollHeight + 'px'
+
+ let lastHeight = 0
+ setInterval(() => {
+ try {
+ const height = iframe.contentWindow.document.body.scrollHeight
+ if (height !== lastHeight) {
+ iframe.style.height = height + 'px'
+ lastHeight = height
+ }
+ } catch (e) {}
+ }, 300)
+ }, 1500)
+ return
+ }
+
+ this.wrapper.innerHTML = ``
+ }
+
+ renderActivityModal() {
+ if (this.readOnly) return
+
+ const app = createApp(AssessmentPlugin, {
+ type: 'wordHunt',
+ onAddition: (activity) => {
+ this.data.activity = activity
+ this.renderActivity(activity)
+ },
+ })
+ app.use(translationPlugin)
+ app.mount(this.wrapper)
+ }
+
+ save() {
+ if (Object.keys(this.data).length === 0) return {}
+ return {
+ activity: this.data.activity,
+ }
+ }
+}
diff --git a/lms/hooks.py b/lms/hooks.py
index f1743f978..5151ce760 100644
--- a/lms/hooks.py
+++ b/lms/hooks.py
@@ -254,6 +254,7 @@ def get_lms_path():
"Video": "lms.plugins.video_renderer",
"Assignment": "lms.plugins.assignment_renderer",
"DragDrop": "lms.plugins.drag_drop_renderer",
+ "WordHunt": "lms.plugins.word_hunt_renderer",
"Embed": "lms.plugins.embed_renderer",
"Audio": "lms.plugins.audio_renderer",
"PDF": "lms.plugins.pdf_renderer",
diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py
index 1b1d7d662..eae67eb34 100644
--- a/lms/lms/doctype/course_lesson/course_lesson.py
+++ b/lms/lms/doctype/course_lesson/course_lesson.py
@@ -79,6 +79,18 @@ def save_lesson_assessment_details(self, content):
"lesson": self.name,
},
)
+ if block.get("type") == "wordHunt":
+ activity = block.get("data").get("activity")
+ if not frappe.db.exists("LMS Word Hunt Activity", activity):
+ frappe.throw(_("Invalid Word Hunt Activity ID in content"))
+ frappe.db.set_value(
+ "LMS Word Hunt Activity",
+ activity,
+ {
+ "course": self.course,
+ "lesson": self.name,
+ },
+ )
@frappe.whitelist()
@@ -101,6 +113,7 @@ def save_progress(lesson: str, course: str, scorm_details: dict = None):
quiz_completed = get_quiz_progress(lesson)
drag_drop_completed = get_drag_drop_progress(lesson)
+ word_hunt_completed = get_word_hunt_progress(lesson)
assignment_completed = get_assignment_progress(lesson)
if scorm_details:
@@ -110,6 +123,7 @@ def save_progress(lesson: str, course: str, scorm_details: dict = None):
not progress_already_exists
and quiz_completed
and drag_drop_completed
+ and word_hunt_completed
and assignment_completed
and not scorm_details
):
@@ -231,6 +245,37 @@ def get_drag_drop_progress(lesson):
return True
+def get_word_hunt_progress(lesson):
+ lesson_details = frappe.db.get_value("Course Lesson", lesson, ["body", "content"], as_dict=1)
+ activities = []
+
+ if lesson_details.content:
+ content = json.loads(lesson_details.content)
+
+ for block in content.get("blocks"):
+ if block.get("type") == "wordHunt":
+ activities.append(block.get("data").get("activity"))
+
+ elif lesson_details.body:
+ macros = find_macros(lesson_details.body)
+ activities = [value for name, value in macros if name == "WordHunt"]
+
+ for activity in activities:
+ passing_percentage = frappe.db.get_value(
+ "LMS Word Hunt Activity", activity, "passing_percentage"
+ )
+ if not frappe.db.exists(
+ "LMS Word Hunt Submission",
+ {
+ "activity": activity,
+ "member": frappe.session.user,
+ "percentage": [">=", passing_percentage],
+ },
+ ):
+ return False
+ return True
+
+
def get_assignment_progress(lesson):
lesson_details = frappe.db.get_value("Course Lesson", lesson, ["body", "content"], as_dict=1)
assignments = []
diff --git a/lms/lms/doctype/lms_batch/lms_batch.js b/lms/lms/doctype/lms_batch/lms_batch.js
index fb6fee9d6..7442c99b5 100644
--- a/lms/lms/doctype/lms_batch/lms_batch.js
+++ b/lms/lms/doctype/lms_batch/lms_batch.js
@@ -4,7 +4,7 @@
frappe.ui.form.on("LMS Batch", {
onload: function (frm) {
frm.set_query("reference_doctype", "timetable", function () {
- let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment", "LMS Drag Drop Activity"];
+ let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment", "LMS Drag Drop Activity", "LMS Word Hunt Activity"];
return {
filters: {
name: ["in", doctypes],
@@ -21,7 +21,7 @@ frappe.ui.form.on("LMS Batch", {
});
frm.set_query("assessment_type", "assessment", function () {
- let doctypes = ["LMS Quiz", "LMS Assignment", "LMS Drag Drop Activity"];
+ let doctypes = ["LMS Quiz", "LMS Assignment", "LMS Drag Drop Activity", "LMS Word Hunt Activity"];
return {
filters: {
name: ["in", doctypes],
@@ -30,7 +30,7 @@ frappe.ui.form.on("LMS Batch", {
});
frm.set_query("reference_doctype", "timetable_legends", function () {
- let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment", "LMS Drag Drop Activity"];
+ let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment", "LMS Drag Drop Activity", "LMS Word Hunt Activity"];
return {
filters: {
name: ["in", doctypes],
@@ -167,6 +167,11 @@ const set_default_legends = (frm) => {
label: "LMS Drag Drop Activity",
color: "#F97316",
},
+ {
+ reference_doctype: "LMS Word Hunt Activity",
+ label: "LMS Word Hunt Activity",
+ color: "#14B8A6",
+ },
{
reference_doctype: "LMS Live Class",
label: "LMS Live Class",
diff --git a/lms/lms/doctype/lms_word_hunt_activity/__init__.py b/lms/lms/doctype/lms_word_hunt_activity/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lms/lms/doctype/lms_word_hunt_activity/lms_word_hunt_activity.json b/lms/lms/doctype/lms_word_hunt_activity/lms_word_hunt_activity.json
new file mode 100644
index 000000000..e687cd2fc
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_activity/lms_word_hunt_activity.json
@@ -0,0 +1,196 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-03-31 00:00:00.000000",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "passage",
+ "max_attempts",
+ "show_answers",
+ "show_submission_history",
+ "column_break_eosb",
+ "total_marks",
+ "passing_percentage",
+ "duration",
+ "section_break_kxhc",
+ "shuffle_answers",
+ "section_break_hgik",
+ "items",
+ "section_break_jlmr",
+ "lesson",
+ "column_break_hxby",
+ "course"
+ ],
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "passage",
+ "fieldtype": "Long Text",
+ "label": "Passage",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "max_attempts",
+ "fieldtype": "Int",
+ "label": "Max Attempts"
+ },
+ {
+ "default": "1",
+ "fieldname": "show_answers",
+ "fieldtype": "Check",
+ "label": "Show Answers"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_submission_history",
+ "fieldtype": "Check",
+ "label": "Show Submission History"
+ },
+ {
+ "fieldname": "column_break_eosb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "total_marks",
+ "fieldtype": "Int",
+ "label": "Total Marks",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "100",
+ "fieldname": "passing_percentage",
+ "fieldtype": "Int",
+ "label": "Passing Percentage",
+ "non_negative": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "duration",
+ "fieldtype": "Data",
+ "label": "Duration (in minutes)"
+ },
+ {
+ "fieldname": "section_break_kxhc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "shuffle_answers",
+ "fieldtype": "Check",
+ "label": "Shuffle Answers"
+ },
+ {
+ "fieldname": "section_break_hgik",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "options": "LMS Word Hunt Item"
+ },
+ {
+ "fieldname": "section_break_jlmr",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "lesson",
+ "fieldtype": "Link",
+ "label": "Lesson",
+ "options": "Course Lesson",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "lesson.course",
+ "fieldname": "course",
+ "fieldtype": "Link",
+ "label": "Course",
+ "options": "LMS Course",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_hxby",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "link_doctype": "LMS Word Hunt Submission",
+ "link_fieldname": "activity"
+ }
+ ],
+ "modified": "2026-03-31 00:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "LMS",
+ "name": "LMS Word Hunt Activity",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Moderator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Course Creator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "LMS Student",
+ "share": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "show_title_field_in_link": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "title",
+ "track_changes": 1
+}
diff --git a/lms/lms/doctype/lms_word_hunt_activity/lms_word_hunt_activity.py b/lms/lms/doctype/lms_word_hunt_activity/lms_word_hunt_activity.py
new file mode 100644
index 000000000..c98ceab58
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_activity/lms_word_hunt_activity.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2026, Frappe and contributors
+# For license information, please see license.txt
+
+import json
+import re
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cint
+
+from ..course_lesson.course_lesson import save_progress
+from ...utils import generate_slug
+
+BLANK_PATTERN = re.compile(r"_+")
+
+
+class LMSWordHuntActivity(Document):
+ def validate(self):
+ self.validate_passage()
+ self.validate_items()
+ self.validate_placeholders()
+ self.calculate_total_marks()
+
+ def autoname(self):
+ if not self.name:
+ self.name = generate_slug(self.title, "LMS Word Hunt Activity")
+
+ def validate_passage(self):
+ if not self.passage or not self.passage.strip():
+ frappe.throw(_("The passage is required."))
+
+ def validate_items(self):
+ if not self.items:
+ frappe.throw(_("Add at least one blank to this activity."))
+
+ for index, row in enumerate(self.items, start=1):
+ row.placeholder = f"blank_{index}"
+ if not row.correct_answer:
+ frappe.throw(_("Each blank must have a correct answer."))
+
+ def validate_placeholders(self):
+ blank_count = count_blanks(self.passage)
+
+ if not blank_count:
+ frappe.throw(
+ _("Add at least one blank in the passage using underscores like _ or _______.")
+ )
+
+ if blank_count != len(self.items):
+ frappe.throw(
+ _(
+ "The passage contains {0} blanks, but you added {1} answers. The counts must match."
+ ).format(blank_count, len(self.items))
+ )
+
+ def calculate_total_marks(self):
+ self.total_marks = sum(cint(row.marks) for row in self.items)
+ if not self.items:
+ self.total_marks = 0
+ self.passing_percentage = 100
+
+
+def normalize_answer(answer):
+ return " ".join((answer or "").strip().split()).casefold()
+
+
+def count_blanks(passage):
+ return len(BLANK_PATTERN.findall(passage or ""))
+
+
+@frappe.whitelist()
+def submit_activity(activity: str, answers: str):
+ answers = json.loads(answers or "[]")
+ activity_doc = frappe.get_doc("LMS Word Hunt Activity", activity)
+
+ results = []
+ score = 0
+ for item in activity_doc.items:
+ submitted_answer = next(
+ (
+ row.get("submitted_answer")
+ for row in answers
+ if row.get("item") == item.name
+ or row.get("idx") == item.idx
+ ),
+ "",
+ )
+ is_correct = normalize_answer(submitted_answer) == normalize_answer(item.correct_answer)
+ marks = cint(item.marks) if is_correct else 0
+ score += marks
+ results.append(
+ {
+ "item": item.name,
+ "placeholder": item.placeholder,
+ "submitted_answer": submitted_answer,
+ "correct_answer": item.correct_answer,
+ "is_correct": 1 if is_correct else 0,
+ "marks": marks,
+ "marks_out_of": item.marks,
+ }
+ )
+
+ score_out_of = activity_doc.total_marks
+ percentage = (score / score_out_of) * 100 if score_out_of else 0
+ submission = create_submission(activity_doc, results, score, score_out_of, percentage)
+
+ save_progress_after_activity(activity_doc, percentage)
+
+ return {
+ "score": score,
+ "score_out_of": score_out_of,
+ "submission": submission.name,
+ "pass": percentage >= activity_doc.passing_percentage,
+ "percentage": percentage,
+ }
+
+
+def create_submission(activity_doc, results, score, score_out_of, percentage):
+ submission = frappe.new_doc("LMS Word Hunt Submission")
+ submission.update(
+ {
+ "activity": activity_doc.name,
+ "result": results,
+ "score": score,
+ "score_out_of": score_out_of,
+ "member": frappe.session.user,
+ "percentage": percentage,
+ "passing_percentage": activity_doc.passing_percentage,
+ }
+ )
+ submission.save(ignore_permissions=True)
+ return submission
+
+
+def save_progress_after_activity(activity_doc, percentage):
+ if not activity_doc.lesson or not activity_doc.course:
+ return
+
+ if percentage >= activity_doc.passing_percentage or not activity_doc.passing_percentage:
+ save_progress(activity_doc.lesson, activity_doc.course)
diff --git a/lms/lms/doctype/lms_word_hunt_activity/test_lms_word_hunt_activity.py b/lms/lms/doctype/lms_word_hunt_activity/test_lms_word_hunt_activity.py
new file mode 100644
index 000000000..33065629f
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_activity/test_lms_word_hunt_activity.py
@@ -0,0 +1,91 @@
+# Copyright (c) 2026, Frappe and Contributors
+# See license.txt
+
+import unittest
+
+import frappe
+
+
+class TestLMSWordHuntActivity(unittest.TestCase):
+ def test_total_marks_are_calculated(self):
+ activity = frappe.get_doc(
+ {
+ "doctype": "LMS Word Hunt Activity",
+ "title": "Word Hunt Marks Activity",
+ "passing_percentage": 100,
+ "passage": "I _______ to school and _______ lunch there.",
+ "items": [
+ {
+ "doctype": "LMS Word Hunt Item",
+ "correct_answer": "go",
+ "marks": 2,
+ },
+ {
+ "doctype": "LMS Word Hunt Item",
+ "correct_answer": "eat",
+ "marks": 3,
+ },
+ ],
+ }
+ )
+ activity.insert()
+ self.assertEqual(activity.total_marks, 5)
+
+ def test_passage_blank_count_must_match_items(self):
+ activity = frappe.get_doc(
+ {
+ "doctype": "LMS Word Hunt Activity",
+ "title": "Mismatched Blank Count Activity",
+ "passing_percentage": 100,
+ "passage": "I _______ to school and _______ lunch there.",
+ "items": [
+ {
+ "doctype": "LMS Word Hunt Item",
+ "correct_answer": "go",
+ "marks": 1,
+ }
+ ],
+ }
+ )
+
+ with self.assertRaises(frappe.ValidationError):
+ activity.insert()
+
+ def test_requires_passage_blank(self):
+ activity = frappe.get_doc(
+ {
+ "doctype": "LMS Word Hunt Activity",
+ "title": "Invalid Passage Activity",
+ "passing_percentage": 100,
+ "passage": "This passage has no blanks.",
+ "items": [
+ {
+ "doctype": "LMS Word Hunt Item",
+ "correct_answer": "go",
+ "marks": 1,
+ }
+ ],
+ }
+ )
+
+ with self.assertRaises(frappe.ValidationError):
+ activity.insert()
+
+ def test_single_underscore_counts_as_blank(self):
+ activity = frappe.get_doc(
+ {
+ "doctype": "LMS Word Hunt Activity",
+ "title": "Single Underscore Activity",
+ "passing_percentage": 100,
+ "passage": "I _ to school.",
+ "items": [
+ {
+ "doctype": "LMS Word Hunt Item",
+ "correct_answer": "go",
+ "marks": 1,
+ }
+ ],
+ }
+ )
+ activity.insert()
+ self.assertEqual(activity.total_marks, 1)
diff --git a/lms/lms/doctype/lms_word_hunt_item/__init__.py b/lms/lms/doctype/lms_word_hunt_item/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lms/lms/doctype/lms_word_hunt_item/lms_word_hunt_item.json b/lms/lms/doctype/lms_word_hunt_item/lms_word_hunt_item.json
new file mode 100644
index 000000000..eb987c36b
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_item/lms_word_hunt_item.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "creation": "2026-03-31 00:00:00.000000",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "placeholder",
+ "correct_answer",
+ "marks"
+ ],
+ "fields": [
+ {
+ "fieldname": "placeholder",
+ "fieldtype": "Data",
+ "label": "Placeholder",
+ "read_only": 1
+ },
+ {
+ "fieldname": "correct_answer",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Correct Answer",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "marks",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Marks",
+ "non_negative": 1,
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-03-31 00:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "LMS",
+ "name": "LMS Word Hunt Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
diff --git a/lms/lms/doctype/lms_word_hunt_item/lms_word_hunt_item.py b/lms/lms/doctype/lms_word_hunt_item/lms_word_hunt_item.py
new file mode 100644
index 000000000..3e386e7f6
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_item/lms_word_hunt_item.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2026, Frappe and contributors
+# For license information, please see license.txt
+
+from frappe.model.document import Document
+
+
+class LMSWordHuntItem(Document):
+ pass
diff --git a/lms/lms/doctype/lms_word_hunt_result/__init__.py b/lms/lms/doctype/lms_word_hunt_result/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lms/lms/doctype/lms_word_hunt_result/lms_word_hunt_result.json b/lms/lms/doctype/lms_word_hunt_result/lms_word_hunt_result.json
new file mode 100644
index 000000000..669c14cb0
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_result/lms_word_hunt_result.json
@@ -0,0 +1,80 @@
+{
+ "actions": [],
+ "creation": "2026-03-31 00:00:00.000000",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item",
+ "placeholder",
+ "submitted_answer",
+ "column_break_ixwe",
+ "correct_answer",
+ "is_correct",
+ "marks",
+ "marks_out_of"
+ ],
+ "fields": [
+ {
+ "fieldname": "item",
+ "fieldtype": "Data",
+ "label": "Item"
+ },
+ {
+ "fieldname": "placeholder",
+ "fieldtype": "Data",
+ "label": "Placeholder",
+ "read_only": 1
+ },
+ {
+ "fieldname": "submitted_answer",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Submitted Answer",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_ixwe",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "correct_answer",
+ "fieldtype": "Data",
+ "label": "Correct Answer",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "is_correct",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Correct"
+ },
+ {
+ "fieldname": "marks",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Marks"
+ },
+ {
+ "fieldname": "marks_out_of",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Marks Out Of",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-03-31 00:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "LMS",
+ "name": "LMS Word Hunt Result",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
diff --git a/lms/lms/doctype/lms_word_hunt_result/lms_word_hunt_result.py b/lms/lms/doctype/lms_word_hunt_result/lms_word_hunt_result.py
new file mode 100644
index 000000000..70817dc21
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_result/lms_word_hunt_result.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2026, Frappe and contributors
+# For license information, please see license.txt
+
+from frappe.model.document import Document
+
+
+class LMSWordHuntResult(Document):
+ pass
diff --git a/lms/lms/doctype/lms_word_hunt_submission/__init__.py b/lms/lms/doctype/lms_word_hunt_submission/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/lms/lms/doctype/lms_word_hunt_submission/lms_word_hunt_submission.json b/lms/lms/doctype/lms_word_hunt_submission/lms_word_hunt_submission.json
new file mode 100644
index 000000000..3f5c4f1ee
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_submission/lms_word_hunt_submission.json
@@ -0,0 +1,163 @@
+{
+ "actions": [],
+ "creation": "2026-03-31 00:00:00.000000",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "activity",
+ "activity_title",
+ "column_break_mqku",
+ "member",
+ "member_name",
+ "section_break_iskf",
+ "score",
+ "score_out_of",
+ "column_break_qhbt",
+ "percentage",
+ "passing_percentage",
+ "section_break_uvmw",
+ "result"
+ ],
+ "fields": [
+ {
+ "fieldname": "activity",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Activity",
+ "options": "LMS Word Hunt Activity"
+ },
+ {
+ "fetch_from": "activity.title",
+ "fieldname": "activity_title",
+ "fieldtype": "Data",
+ "label": "Activity Title",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_mqku",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "member",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Member",
+ "options": "User"
+ },
+ {
+ "fetch_from": "member.full_name",
+ "fieldname": "member_name",
+ "fieldtype": "Data",
+ "label": "Member Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_iskf",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "score",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Score",
+ "reqd": 1
+ },
+ {
+ "fieldname": "score_out_of",
+ "fieldtype": "Int",
+ "label": "Score Out Of",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_qhbt",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "percentage",
+ "fieldtype": "Int",
+ "label": "Percentage",
+ "reqd": 1
+ },
+ {
+ "fieldname": "passing_percentage",
+ "fieldtype": "Int",
+ "label": "Passing Percentage",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_uvmw",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "result",
+ "fieldtype": "Table",
+ "label": "Result",
+ "options": "LMS Word Hunt Result"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2026-03-31 00:00:00.000000",
+ "modified_by": "Administrator",
+ "module": "LMS",
+ "name": "LMS Word Hunt Submission",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "LMS Student",
+ "share": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Moderator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Course Creator",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "show_title_field_in_link": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "member_name",
+ "track_changes": 1
+}
diff --git a/lms/lms/doctype/lms_word_hunt_submission/lms_word_hunt_submission.py b/lms/lms/doctype/lms_word_hunt_submission/lms_word_hunt_submission.py
new file mode 100644
index 000000000..e9f5b69de
--- /dev/null
+++ b/lms/lms/doctype/lms_word_hunt_submission/lms_word_hunt_submission.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2026, Frappe and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
+from frappe.model.document import Document
+from frappe.utils import cint
+
+
+class LMSWordHuntSubmission(Document):
+ def validate(self):
+ self.validate_if_max_attempts_exceeded()
+ self.validate_marks()
+ self.set_percentage()
+
+ def on_update(self):
+ self.notify_member()
+
+ def validate_if_max_attempts_exceeded(self):
+ max_attempts = frappe.db.get_value("LMS Word Hunt Activity", self.activity, "max_attempts")
+ if max_attempts == 0:
+ return
+
+ current_user_submission_count = frappe.db.count(
+ self.doctype, filters={"activity": self.activity, "member": frappe.session.user}
+ )
+ if current_user_submission_count >= max_attempts:
+ frappe.throw(
+ _("You have exceeded the maximum number of attempts ({0}) for this activity").format(
+ max_attempts
+ ),
+ MaximumAttemptsExceededError,
+ )
+
+ def validate_marks(self):
+ self.score = 0
+ for row in self.result:
+ if cint(row.marks) > cint(row.marks_out_of):
+ frappe.throw(
+ _(
+ "Marks for row number {0} cannot be greater than the marks allotted for that row."
+ ).format(row.idx)
+ )
+ self.score += cint(row.marks)
+
+ def set_percentage(self):
+ if self.score_out_of:
+ self.percentage = (self.score / self.score_out_of) * 100
+
+ def notify_member(self):
+ if not self.has_value_changed("score"):
+ return
+
+ notification = frappe._dict(
+ {
+ "subject": _("Your score for {0} has been updated to {1}").format(
+ frappe.bold(self.activity_title), frappe.bold(self.score)
+ ),
+ "email_content": _(
+ "There has been an update on your word hunt submission. Your latest score is {0}."
+ ).format(frappe.bold(self.score)),
+ "document_type": self.doctype,
+ "document_name": self.name,
+ "for_user": self.member,
+ "from_user": frappe.session.user,
+ "type": "Alert",
+ "link": "",
+ }
+ )
+ make_notification_logs(notification, [self.member])
+
+
+class MaximumAttemptsExceededError(frappe.DuplicateEntryError):
+ pass
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index 9cf199a8e..5902270b9 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -209,6 +209,8 @@ def get_lesson_icon(body: str, content: str):
return "icon-quiz"
if block.get("type") == "dragDrop":
return "icon-quiz"
+ if block.get("type") == "wordHunt":
+ return "icon-quiz"
if block.get("type") == "assignment":
return "icon-assignment"
if block.get("type") == "program":
@@ -224,6 +226,8 @@ def get_lesson_icon(body: str, content: str):
return "icon-quiz"
elif macro[0] == "DragDrop":
return "icon-quiz"
+ elif macro[0] == "WordHunt":
+ return "icon-quiz"
return "icon-list"
@@ -1271,6 +1275,9 @@ def get_assessments(batch: str) -> list:
elif assessment.assessment_type == "LMS Drag Drop Activity":
assessment = get_drag_drop_details(assessment, member)
+ elif assessment.assessment_type == "LMS Word Hunt Activity":
+ assessment = get_word_hunt_details(assessment, member)
+
elif assessment.assessment_type == "LMS Programming Exercise":
assessment = get_exercise_details(assessment, member)
@@ -1395,6 +1402,38 @@ def get_drag_drop_details(assessment, member):
return assessment
+def get_word_hunt_details(assessment, member):
+ assessment_details = frappe.db.get_value(
+ "LMS Word Hunt Activity", assessment.assessment_name, ["title", "passing_percentage"], as_dict=1
+ )
+ assessment.title = assessment_details.title
+
+ existing_submission = frappe.get_all(
+ "LMS Word Hunt Submission",
+ {
+ "member": member,
+ "activity": assessment.assessment_name,
+ },
+ ["name", "score", "percentage"],
+ order_by="percentage desc",
+ )
+
+ if len(existing_submission):
+ assessment.submission = existing_submission[0]
+ assessment.completed = True
+ assessment.status = assessment.submission.percentage or assessment.submission.score
+ else:
+ assessment.status = "Not Attempted"
+ assessment.color = "red"
+ assessment.completed = False
+
+ assessment.edit_url = f"/word-hunt-activities/{assessment.assessment_name}"
+ submission_name = existing_submission[0].name if len(existing_submission) else "new-submission"
+ assessment.url = f"/word-hunt-submission/{submission_name}"
+
+ return assessment
+
+
@frappe.whitelist()
def get_batch_student_progress(member: str, batch: str) -> dict:
if not can_modify_batch(batch):
@@ -1512,6 +1551,33 @@ def get_drag_drop_pass_stats(batch):
return [{"task": row.title, "value": row.passed or 0} for row in rows]
+def get_word_hunt_pass_stats(batch):
+ Assessment = frappe.qb.DocType("LMS Assessment")
+ Activity = frappe.qb.DocType("LMS Word Hunt Activity")
+ BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
+ Submission = frappe.qb.DocType("LMS Word Hunt Submission")
+
+ rows = (
+ frappe.qb.from_(Assessment)
+ .join(Activity)
+ .on(Activity.name == Assessment.assessment_name)
+ .left_join(BatchEnrollment)
+ .on(BatchEnrollment.batch == Assessment.parent)
+ .left_join(Submission)
+ .on((Submission.activity == Assessment.assessment_name) & (Submission.member == BatchEnrollment.member))
+ .where((Assessment.parent == batch) & (Assessment.assessment_type == "LMS Word Hunt Activity"))
+ .groupby(Assessment.assessment_name, Activity.title)
+ .select(
+ Activity.title,
+ fn.Count(Case().when(Submission.percentage >= Submission.passing_percentage, Submission.member))
+ .distinct()
+ .as_("passed"),
+ )
+ ).run(as_dict=True)
+
+ return [{"task": row.title, "value": row.passed or 0} for row in rows]
+
+
@frappe.whitelist()
def get_batch_chart_data(batch: str) -> list:
"""Get completion counts per course and assessment"""
@@ -1525,6 +1591,7 @@ def get_batch_chart_data(batch: str) -> list:
+ get_assignment_pass_stats(batch)
+ get_quiz_pass_stats(batch)
+ get_drag_drop_pass_stats(batch)
+ + get_word_hunt_pass_stats(batch)
)
@@ -1638,6 +1705,11 @@ def get_assessment_meta(assessment_type: str):
docfield = "activity"
fields = ["percentage"]
not_attempted = 0
+ elif assessment_type == "LMS Word Hunt Activity":
+ doctype = "LMS Word Hunt Submission"
+ docfield = "activity"
+ fields = ["percentage"]
+ not_attempted = 0
elif assessment_type == "LMS Programming Exercise":
doctype = "LMS Programming Exercise Submission"
docfield = "exercise"
@@ -1663,12 +1735,19 @@ def get_assessment_attempt_details(
)
if attempt_details.percentage >= passing_percentage:
result = "Pass"
+ elif assessment_type == "LMS Word Hunt Activity":
+ result = "Failed"
+ passing_percentage = frappe.db.get_value(
+ "LMS Word Hunt Activity", assessment, "passing_percentage"
+ )
+ if attempt_details.percentage >= passing_percentage:
+ result = "Pass"
else:
result = attempt_details.status
return frappe._dict(
{
"status": attempt_details.percentage
- if assessment_type in ["LMS Quiz", "LMS Drag Drop Activity"]
+ if assessment_type in ["LMS Quiz", "LMS Drag Drop Activity", "LMS Word Hunt Activity"]
else attempt_details.status,
"result": result,
"assessment": assessment,
diff --git a/lms/plugins.py b/lms/plugins.py
index fa26d98d8..a54822634 100644
--- a/lms/plugins.py
+++ b/lms/plugins.py
@@ -240,6 +240,21 @@ def drag_drop_renderer(activity_name):
return f"
{frappe.bold(activity.title)}
"
+def word_hunt_renderer(activity_name):
+ if frappe.session.user == "Guest":
+ return "
" + _(
+ "Word hunt activity is not available to Guest users. Please login to continue."
+ ) + "
"
+
+ activity = frappe.db.get_value(
+ "LMS Word Hunt Activity",
+ activity_name,
+ ["name", "title", "max_attempts", "show_answers", "show_submission_history", "passing_percentage"],
+ as_dict=True,
+ )
+ return f"
{frappe.bold(activity.title)}
"
+
+
def show_custom_signup():
settings = frappe.get_single("LMS Settings")
if settings.custom_signup_content or settings.user_category: