feat: open ended questions
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Choices\nUser Input"
|
||||
"options": "Choices\nUser Input\nOpen Ended"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type == \"Choices\";",
|
||||
@@ -196,7 +196,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-08-01 12:53:22.540990",
|
||||
"modified": "2024-10-07 09:41:17.862774",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Question",
|
||||
|
||||
@@ -17,7 +17,7 @@ def validate_correct_answers(question):
|
||||
if question.type == "Choices":
|
||||
validate_duplicate_options(question)
|
||||
validate_correct_options(question)
|
||||
else:
|
||||
elif question.type == "User Input":
|
||||
validate_possible_answer(question)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
import re
|
||||
from frappe import _, safe_decode
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, comma_and, cint
|
||||
from fuzzywuzzy import fuzz
|
||||
@@ -13,6 +14,9 @@ from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_instructor_role,
|
||||
)
|
||||
from binascii import Error as BinasciiError
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
from frappe.core.doctype.file.utils import get_random_filename
|
||||
|
||||
|
||||
class LMSQuiz(Document):
|
||||
@@ -20,6 +24,7 @@ class LMSQuiz(Document):
|
||||
self.validate_duplicate_questions()
|
||||
self.validate_limit()
|
||||
self.calculate_total_marks()
|
||||
self.validate_open_ended_questions()
|
||||
|
||||
def validate_duplicate_questions(self):
|
||||
questions = [row.question for row in self.questions]
|
||||
@@ -48,6 +53,19 @@ class LMSQuiz(Document):
|
||||
else:
|
||||
self.total_marks = sum(cint(question.marks) for question in self.questions)
|
||||
|
||||
def validate_open_ended_questions(self):
|
||||
types = [question.type for question in self.questions]
|
||||
types = set(types)
|
||||
|
||||
if "Open Ended" in types and len(types) > 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"If you want open ended questions then make sure each question in the quiz is of open ended type."
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.show_answers = 0
|
||||
|
||||
def autoname(self):
|
||||
if not self.name:
|
||||
self.name = generate_slug(self.title, "LMS Quiz")
|
||||
@@ -81,34 +99,50 @@ def set_total_marks(questions):
|
||||
def quiz_summary(quiz, results):
|
||||
score = 0
|
||||
results = results and json.loads(results)
|
||||
is_open_ended = False
|
||||
|
||||
for result in results:
|
||||
correct = result["is_correct"][0]
|
||||
for point in result["is_correct"]:
|
||||
correct = correct and point
|
||||
result["is_correct"] = correct
|
||||
|
||||
question_details = frappe.db.get_value(
|
||||
"LMS Quiz Question",
|
||||
{"parent": quiz, "question": result["question_name"]},
|
||||
["question", "marks", "question_detail"],
|
||||
["question", "marks", "question_detail", "type"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
result["question_name"] = question_details.question
|
||||
result["question"] = question_details.question_detail
|
||||
marks = question_details.marks if correct else 0
|
||||
result["marks_out_of"] = question_details.marks
|
||||
|
||||
result["marks"] = marks
|
||||
score += marks
|
||||
quiz_details = frappe.get_doc(
|
||||
"LMS Quiz",
|
||||
quiz,
|
||||
["total_marks", "passing_percentage", "lesson", "course"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
del result["question_name"]
|
||||
score = 0
|
||||
percentage = 0
|
||||
score_out_of = quiz_details.total_marks
|
||||
|
||||
quiz_details = frappe.db.get_value(
|
||||
"LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1
|
||||
)
|
||||
score_out_of = quiz_details.total_marks
|
||||
percentage = (score / score_out_of) * 100
|
||||
if question_details.type != "Open Ended":
|
||||
correct = result["is_correct"][0]
|
||||
for point in result["is_correct"]:
|
||||
correct = correct and point
|
||||
result["is_correct"] = correct
|
||||
|
||||
marks = question_details.marks if correct else 0
|
||||
result["marks"] = marks
|
||||
score += marks
|
||||
|
||||
del result["question_name"]
|
||||
percentage = (score / score_out_of) * 100
|
||||
else:
|
||||
result["is_correct"] = 0
|
||||
is_open_ended = True
|
||||
|
||||
result["answer"] = re.sub(
|
||||
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
|
||||
)
|
||||
|
||||
submission = frappe.get_doc(
|
||||
{
|
||||
@@ -139,128 +173,51 @@ def quiz_summary(quiz, results):
|
||||
"submission": submission.name,
|
||||
"pass": percentage == quiz_details.passing_percentage,
|
||||
"percentage": percentage,
|
||||
"is_open_ended": is_open_ended,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_quiz(
|
||||
quiz_title,
|
||||
passing_percentage,
|
||||
questions,
|
||||
max_attempts=0,
|
||||
quiz=None,
|
||||
show_answers=1,
|
||||
show_submission_history=0,
|
||||
):
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
return
|
||||
def _save_file(match):
|
||||
data = match.group(1).split("data:")[1]
|
||||
headers, content = data.split(",")
|
||||
mtype = headers.split(";", 1)[0]
|
||||
|
||||
values = {
|
||||
"title": quiz_title,
|
||||
"passing_percentage": passing_percentage,
|
||||
"max_attempts": max_attempts,
|
||||
"show_answers": show_answers,
|
||||
"show_submission_history": show_submission_history,
|
||||
}
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
if b"," in content:
|
||||
content = content.split(b",")[1]
|
||||
|
||||
try:
|
||||
content = safe_b64decode(content)
|
||||
except BinasciiError:
|
||||
frappe.flags.has_dataurl = True
|
||||
return f'<img src="#broken-image" alt="{get_corrupted_image_msg()}"'
|
||||
|
||||
if "filename=" in headers:
|
||||
filename = headers.split("filename=")[-1]
|
||||
filename = safe_decode(filename).split(";", 1)[0]
|
||||
|
||||
if quiz:
|
||||
frappe.db.set_value("LMS Quiz", quiz, values)
|
||||
update_questions(quiz, questions)
|
||||
return quiz
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Quiz")
|
||||
doc.update(values)
|
||||
doc.save()
|
||||
update_questions(doc.name, questions)
|
||||
return doc.name
|
||||
filename = get_random_filename(content_type=mtype)
|
||||
|
||||
|
||||
def update_questions(quiz, questions):
|
||||
questions = json.loads(questions)
|
||||
|
||||
delete_questions(quiz, questions)
|
||||
add_questions(quiz, questions)
|
||||
frappe.db.set_value("LMS Quiz", quiz, "total_marks", set_total_marks(quiz, questions))
|
||||
|
||||
|
||||
def delete_questions(quiz, questions):
|
||||
existing_questions = frappe.get_all(
|
||||
"LMS Quiz Question",
|
||||
_file = frappe.get_doc(
|
||||
{
|
||||
"parent": quiz,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
current_questions = [question.get("question_name") for question in questions]
|
||||
|
||||
for question in existing_questions:
|
||||
if question not in current_questions:
|
||||
frappe.db.delete("LMS Quiz Question", question)
|
||||
|
||||
|
||||
def add_questions(quiz, questions):
|
||||
for index, question in enumerate(questions):
|
||||
question = frappe._dict(question)
|
||||
if question.question_name:
|
||||
doc = frappe.get_doc("LMS Quiz Question", question.question_name)
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Quiz Question")
|
||||
doc.update(
|
||||
{
|
||||
"parent": quiz,
|
||||
"parenttype": "LMS Quiz",
|
||||
"parentfield": "questions",
|
||||
"idx": index + 1,
|
||||
}
|
||||
)
|
||||
|
||||
doc.update({"question": question.question, "marks": question.marks})
|
||||
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_question(quiz, values, index):
|
||||
values = frappe._dict(json.loads(values))
|
||||
|
||||
if values.get("name"):
|
||||
doc = frappe.get_doc("LMS Question", values.get("name"))
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Question")
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"question": values.question,
|
||||
"type": values["type"],
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"content": content,
|
||||
"decode": False,
|
||||
"is_private": False,
|
||||
}
|
||||
)
|
||||
_file.save(ignore_permissions=True)
|
||||
file_url = _file.unique_url
|
||||
frappe.flags.has_dataurl = True
|
||||
|
||||
for num in range(1, 5):
|
||||
if values.get(f"option_{num}"):
|
||||
doc.update(
|
||||
{
|
||||
f"option_{num}": values[f"option_{num}"],
|
||||
f"is_correct_{num}": values[f"is_correct_{num}"],
|
||||
}
|
||||
)
|
||||
return f'<img src="{file_url}"'
|
||||
|
||||
if values.get(f"explanation_{num}"):
|
||||
doc.update(
|
||||
{
|
||||
f"explanation_{num}": values[f"explanation_{num}"],
|
||||
}
|
||||
)
|
||||
|
||||
if values.get(f"possibility_{num}"):
|
||||
doc.update(
|
||||
{
|
||||
f"possibility_{num}": values[f"possibility_{num}"],
|
||||
}
|
||||
)
|
||||
|
||||
doc.save()
|
||||
return doc.name
|
||||
def get_corrupted_image_msg():
|
||||
return _("Image: Corrupted Data Stream")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -318,9 +275,3 @@ def check_input_answers(question, answer):
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_user_quizzes():
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
return frappe.get_all("LMS Quiz", filters=filters, fields=["name", "title"])
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"column_break_qcpo",
|
||||
"marks",
|
||||
"section_break_huup",
|
||||
"question_detail"
|
||||
"question_detail",
|
||||
"type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -44,12 +45,21 @@
|
||||
{
|
||||
"fieldname": "section_break_huup",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "question.type",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Choices\nUser Input\nOpen Ended",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-29 15:10:09.662715",
|
||||
"modified": "2024-10-07 15:01:38.800906",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Question",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"answer",
|
||||
"column_break_flus",
|
||||
"marks",
|
||||
"marks_out_of",
|
||||
"is_correct"
|
||||
],
|
||||
"fields": [
|
||||
@@ -33,8 +34,7 @@
|
||||
"fieldname": "is_correct",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Correct",
|
||||
"read_only": 1
|
||||
"label": "Is Correct"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fztv",
|
||||
@@ -54,14 +54,20 @@
|
||||
"fieldname": "marks",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Marks",
|
||||
"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": "2024-05-17 17:38:51.760653",
|
||||
"modified": "2024-10-07 17:28:38.597472",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Result",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"quiz",
|
||||
"quiz_title",
|
||||
"course",
|
||||
"column_break_3",
|
||||
"member",
|
||||
@@ -39,7 +40,6 @@
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Score",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -95,7 +95,6 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Percentage",
|
||||
"non_negative": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -105,12 +104,19 @@
|
||||
"non_negative": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "quiz.title",
|
||||
"fieldname": "quiz_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Quiz Title",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-27 13:01:53.611726",
|
||||
"modified": "2024-10-07 16:52:04.162521",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Submission",
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe import _
|
||||
|
||||
|
||||
class LMSQuizSubmission(Document):
|
||||
def before_insert(self):
|
||||
if not self.percentage:
|
||||
self.set_percentage()
|
||||
def validate(self):
|
||||
self.validate_marks()
|
||||
self.set_percentage()
|
||||
|
||||
def validate_marks(self):
|
||||
for row in self.result:
|
||||
if cint(row.marks) > cint(row.marks_out_of):
|
||||
frappe.throw(
|
||||
_(
|
||||
f"Marks for question number {row.idx} cannot be greater than the marks allotted for that question."
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.score += cint(row.marks)
|
||||
|
||||
def set_percentage(self):
|
||||
if self.score and self.score_out_of:
|
||||
|
||||
Reference in New Issue
Block a user