feat: quiz with user input

This commit is contained in:
Jannat Patel
2023-03-23 22:22:57 +05:30
parent 3b1b375d5b
commit 3150cf2510
6 changed files with 200 additions and 35 deletions

View File

@@ -21,17 +21,39 @@ class LMSQuiz(Document):
def validate_correct_answers(self): def validate_correct_answers(self):
for question in self.questions: for question in self.questions:
correct_options = self.get_correct_options(question) if question.type == "Choices":
self.validate_correct_options(question)
else:
self.validate_possible_answer(question)
if len(correct_options) > 1: def validate_correct_options(self, question):
question.multiple = 1 correct_options = self.get_correct_options(question)
if not len(correct_options): if len(correct_options) > 1:
frappe.throw( question.multiple = 1
_("At least one option must be correct for this question: {0}").format(
frappe.bold(question.question) if not len(correct_options):
) frappe.throw(
_("At least one option must be correct for this question: {0}").format(
frappe.bold(question.question)
) )
)
def validate_possible_answer(self, question):
possible_answers_fields = [
"possibility_1",
"possibility_2",
"possibility_3",
"possibility_4",
]
possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields))
if not len(possible_answers):
frappe.throw(
_("Add at least one possible answer for this question: {0}").format(
frappe.bold(question.question)
)
)
def get_correct_options(self, question): def get_correct_options(self, question):
correct_option_fields = [ correct_option_fields = [
@@ -140,3 +162,41 @@ def save_quiz(quiz_title, questions, quiz):
question_doc.save(ignore_permissions=True) question_doc.save(ignore_permissions=True)
return doc.name return doc.name
@frappe.whitelist()
def check_answer(question, type, answer):
if type == "Choices":
return check_choice_answers(question, answer)
else:
return check_input_answers(question, answer)
def check_choice_answers(question, answer):
fields = []
for num in range(1, 5):
fields.append(f"option_{cstr(num)}")
fields.append(f"is_correct_{cstr(num)}")
question_details = frappe.db.get_value(
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5):
if question_details[f"option_{num}"] == answer:
return question_details[f"is_correct_{num}"]
return 0
def check_input_answers(question, answer):
fields = []
for num in range(1, 5):
fields.append(f"possibility_{cstr(num)}")
question_details = frappe.db.get_value(
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5):
if question_details[f"possibility_{num}"] == answer:
return 1
return 0

View File

@@ -6,6 +6,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"type",
"options_section", "options_section",
"option_1", "option_1",
"is_correct_1", "is_correct_1",
@@ -26,6 +27,13 @@
"is_correct_4", "is_correct_4",
"column_break_20", "column_break_20",
"explanation_4", "explanation_4",
"section_break_mnhr",
"possibility_1",
"possibility_3",
"column_break_vnaj",
"possibility_2",
"possibility_4",
"section_break_c1lf",
"multiple" "multiple"
], ],
"fields": [ "fields": [
@@ -40,13 +48,13 @@
"fieldname": "option_1", "fieldname": "option_1",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Option 1", "label": "Option 1",
"reqd": 1 "mandatory_depends_on": "eval: doc.type == 'Choices'"
}, },
{ {
"fieldname": "option_2", "fieldname": "option_2",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Option 2", "label": "Option 2",
"reqd": 1 "mandatory_depends_on": "eval: doc.type == 'Choices'"
}, },
{ {
"fieldname": "option_3", "fieldname": "option_3",
@@ -95,18 +103,22 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "options_section", "fieldname": "options_section",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_11", "fieldname": "section_break_11",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
@@ -149,12 +161,52 @@
{ {
"fieldname": "column_break_20", "fieldname": "column_break_20",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Choices\nUser Input"
},
{
"depends_on": "eval: doc.type == 'User Input'",
"fieldname": "section_break_mnhr",
"fieldtype": "Section Break"
},
{
"fieldname": "possibility_1",
"fieldtype": "Small Text",
"label": "Possible Answer 1",
"mandatory_depends_on": "eval: doc.type == 'User Input'"
},
{
"fieldname": "possibility_2",
"fieldtype": "Small Text",
"label": "Possible Answer 2"
},
{
"fieldname": "possibility_3",
"fieldtype": "Small Text",
"label": "Possible Answer 3"
},
{
"fieldname": "possibility_4",
"fieldtype": "Small Text",
"label": "Possible Answer 4"
},
{
"fieldname": "section_break_c1lf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vnaj",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-19 19:35:28.446236", "modified": "2023-03-17 18:22:20.324536",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Question", "name": "LMS Quiz Question",
@@ -162,5 +214,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -48,3 +48,4 @@ lms.patches.v0_0.user_singles_issue #23-11-2022
lms.patches.v0_0.rename_community_to_users #06-01-2023 lms.patches.v0_0.rename_community_to_users #06-01-2023
lms.patches.v0_0.video_embed_link lms.patches.v0_0.video_embed_link
lms.patches.v0_0.rename_exercise_doctype lms.patches.v0_0.rename_exercise_doctype
lms.patches.v0_0.add_question_type

View File

@@ -0,0 +1,8 @@
import frappe
def execute():
questions = frappe.get_all("LMS Quiz Question", pluck="name")
for question in questions:
frappe.db.set_value("LMS Quiz Question", question, "type", "Choices")

View File

@@ -46,9 +46,9 @@
<form id="quiz-form" class="hide"> <form id="quiz-form" class="hide">
<div class="questions"> <div class="questions">
{% for question in quiz.questions %} {% for question in quiz.questions %}
{% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %} {% set instruction = _("Choose all answers that apply") if question.type == "Choices" and question.multiple else _("Choose 1 answer") if question.type == "Choices" else _("Enter the correct answer") %}
<div class="question hide" <div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
data-question="{{ question.question }}" data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}"> data-question="{{ question.question }}" data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
<div class="question-header"> <div class="question-header">
<div class="question-number">{{ loop.index }}. </div> <div class="question-number">{{ loop.index }}. </div>
@@ -58,6 +58,7 @@
<div class="small"> {{ instruction }} </div> <div class="small"> {{ instruction }} </div>
</div> </div>
{% if question.type == "Choices" %}
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
{% for option in options %} {% for option in options %}
{% if option %} {% if option %}
@@ -66,8 +67,7 @@
<label class="quiz-label"> <label class="quiz-label">
<div class="course-meta font-weight-bold"> {{ convert_number_to_character(loop.index - 1) }}</div> <div class="course-meta font-weight-bold"> {{ convert_number_to_character(loop.index - 1) }}</div>
<input class="option" value="{{ option | urlencode }}" <input class="option" value="{{ option | urlencode }}"
data-correct="{{ question['is_correct_' + loop.index | string] }}" {% if question.multiple %} {% if question.multiple %} type="checkbox" {% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
type="checkbox" {% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
<div class="option-text">{{ frappe.utils.md_to_html(option) }}</div> <div class="option-text">{{ frappe.utils.md_to_html(option) }}</div>
</label> </label>
</div> </div>
@@ -80,6 +80,14 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% else %}
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control bold possibility" style="height: 150px;" spellcheck="false"></textarea>
</div>
</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -2,6 +2,8 @@ frappe.ready(() => {
this.marked_as_complete = false; this.marked_as_complete = false;
this.quiz_submitted = false; this.quiz_submitted = false;
this.file_type; this.file_type;
this.answer = [];
this.is_correct = [];
let self = this; let self = this;
localStorage.removeItem($("#quiz-title").data("name")); localStorage.removeItem($("#quiz-title").data("name"));
@@ -16,6 +18,10 @@ frappe.ready(() => {
enable_check(e); enable_check(e);
}); });
$(".possibility").keyup((e) => {
enable_check(e);
});
$(window).scroll(() => { $(window).scroll(() => {
let self = this; let self = this;
if ( if (
@@ -37,6 +43,7 @@ frappe.ready(() => {
}); });
$("#next").click((e) => { $("#next").click((e) => {
add_to_local_storage();
mark_active_question(e); mark_active_question(e);
}); });
@@ -224,29 +231,56 @@ const check_answer = (e = undefined) => {
} else { } else {
$("#next").removeClass("hide"); $("#next").removeClass("hide");
} }
let [answer, is_correct] = parse_options(); parse_options();
add_to_local_storage(current_index, answer, is_correct);
}; };
const parse_options = () => { const parse_options = () => {
let answer = []; let type = $(".active-question").data("type");
let is_correct = [];
$(".active-question input").each((i, element) => { if (type == "Choices") {
let correct = parseInt($(element).attr("data-correct")); $(".active-question input").each((i, element) => {
if ($(element).prop("checked")) { is_answer_correct(type, element);
answer.push(decodeURIComponent($(element).val())); });
correct && is_correct.push(1); } else {
correct ? add_icon(element, "check") : add_icon(element, "wrong"); is_answer_correct(type, $(".active-question textarea"));
} else { }
correct && is_correct.push(0); };
correct
? add_icon(element, "minus-circle-green") const is_answer_correct = (type, element) => {
: add_icon(element, "minus-circle"); let answer = type == "Choices" ? decodeURIComponent($(element).val()) : "";
}
frappe.call({
async: false,
method: "lms.lms.doctype.lms_quiz.lms_quiz.check_answer",
args: {
question: $(".active-question").data("name"),
type: type,
answer: answer,
},
callback: (data) => {
type == "Choices"
? parse_choices(element, data.message)
: parse_possible_answers(e);
},
}); });
};
return [answer, is_correct]; const parse_choices = (element, correct) => {
if ($(element).prop("checked")) {
self.answer.push(decodeURIComponent($(element).val()));
correct && self.is_correct.push(1);
correct ? add_icon(element, "check") : add_icon(element, "wrong");
} else {
correct && self.is_correct.push(0);
correct
? add_icon(element, "minus-circle-green")
: add_icon(element, "minus-circle");
}
};
const parse_possible_answers = () => {
self.answer.push(decodeURIComponent($(element).val()));
correct ? self.is_correct.push(1) : self.is_correct.push(0);
}; };
const add_icon = (element, icon) => { const add_icon = (element, icon) => {
@@ -261,14 +295,15 @@ const add_icon = (element, icon) => {
//$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`); //$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`);
}; };
const add_to_local_storage = (current_index, answer, is_correct) => { const add_to_local_storage = () => {
let current_index = $(".active-question").attr("data-qt-index");
let quiz_name = $("#quiz-title").data("name"); let quiz_name = $("#quiz-title").data("name");
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name)); let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
let quiz_obj = { let quiz_obj = {
question_index: current_index, question_index: current_index,
answer: answer.join(), answer: self.answer.join(),
is_correct: is_correct, is_correct: self.is_correct,
}; };
quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]); quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]);