feat: quiz with user input
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
8
lms/patches/v0_0/add_question_type.py
Normal file
8
lms/patches/v0_0/add_question_type.py
Normal 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")
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user