feat: open ended questions

This commit is contained in:
Jannat Patel
2024-10-07 21:18:42 +05:30
parent fc81f1aa26
commit 6d41e4e552
22 changed files with 552 additions and 221 deletions

View File

@@ -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",

View File

@@ -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)

View File

@@ -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"])

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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: