Merge pull request #491 from pateljannat/quiz-input
feat: quiz with user input
This commit is contained in:
@@ -21,17 +21,39 @@ class LMSQuiz(Document):
|
||||
|
||||
def validate_correct_answers(self):
|
||||
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:
|
||||
question.multiple = 1
|
||||
def validate_correct_options(self, question):
|
||||
correct_options = self.get_correct_options(question)
|
||||
|
||||
if not len(correct_options):
|
||||
frappe.throw(
|
||||
_("At least one option must be correct for this question: {0}").format(
|
||||
frappe.bold(question.question)
|
||||
)
|
||||
if len(correct_options) > 1:
|
||||
question.multiple = 1
|
||||
|
||||
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):
|
||||
correct_option_fields = [
|
||||
@@ -126,17 +148,47 @@ def save_quiz(quiz_title, questions, quiz):
|
||||
}
|
||||
)
|
||||
|
||||
question_doc.update({"question": row["question"], "multiple": row["multiple"]})
|
||||
|
||||
for num in range(1, 5):
|
||||
question_doc.update(
|
||||
{
|
||||
"option_" + cstr(num): row["option_" + cstr(num)],
|
||||
"explanation_" + cstr(num): row["explanation_" + cstr(num)],
|
||||
"is_correct_" + cstr(num): row["is_correct_" + cstr(num)],
|
||||
}
|
||||
)
|
||||
|
||||
question_doc.update(row)
|
||||
question_doc.save(ignore_permissions=True)
|
||||
|
||||
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):
|
||||
current_possibility = question_details[f"possibility_{num}"]
|
||||
if current_possibility and current_possibility.lower() == answer.lower():
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
@@ -19,7 +19,8 @@ class TestLMSQuiz(unittest.TestCase):
|
||||
quiz.append(
|
||||
"questions",
|
||||
{
|
||||
"question": "Question multiple",
|
||||
"question": "Question Multiple",
|
||||
"type": "Choices",
|
||||
"option_1": "Option 1",
|
||||
"is_correct_1": 1,
|
||||
"option_2": "Option 2",
|
||||
@@ -35,12 +36,24 @@ class TestLMSQuiz(unittest.TestCase):
|
||||
"questions",
|
||||
{
|
||||
"question": "Question no correct option",
|
||||
"type": "Choices",
|
||||
"option_1": "Option 1",
|
||||
"option_2": "Option 2",
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
||||
|
||||
def test_with_no_possible_answers(self):
|
||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
||||
quiz.append(
|
||||
"questions",
|
||||
{
|
||||
"question": "Question Possible Answers",
|
||||
"type": "User Input",
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
frappe.db.delete("LMS Quiz", "test-quiz")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"question",
|
||||
"type",
|
||||
"options_section",
|
||||
"option_1",
|
||||
"is_correct_1",
|
||||
@@ -26,6 +27,13 @@
|
||||
"is_correct_4",
|
||||
"column_break_20",
|
||||
"explanation_4",
|
||||
"section_break_mnhr",
|
||||
"possibility_1",
|
||||
"possibility_3",
|
||||
"column_break_vnaj",
|
||||
"possibility_2",
|
||||
"possibility_4",
|
||||
"section_break_c1lf",
|
||||
"multiple"
|
||||
],
|
||||
"fields": [
|
||||
@@ -40,13 +48,13 @@
|
||||
"fieldname": "option_1",
|
||||
"fieldtype": "Data",
|
||||
"label": "Option 1",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
||||
},
|
||||
{
|
||||
"fieldname": "option_2",
|
||||
"fieldtype": "Data",
|
||||
"label": "Option 2",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
||||
},
|
||||
{
|
||||
"fieldname": "option_3",
|
||||
@@ -95,18 +103,22 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Choices'",
|
||||
"fieldname": "options_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Choices'",
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Choices'",
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.type == 'Choices'",
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
@@ -149,12 +161,52 @@
|
||||
{
|
||||
"fieldname": "column_break_20",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-19 19:35:28.446236",
|
||||
"modified": "2023-03-17 18:22:20.324536",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Question",
|
||||
@@ -162,5 +214,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"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.video_embed_link
|
||||
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")
|
||||
@@ -90,6 +90,7 @@ input[type=checkbox] {
|
||||
padding: 2rem 0 5rem;
|
||||
padding-top: 3rem;
|
||||
background-color: var(--bg-color);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.common-card-style {
|
||||
@@ -425,7 +426,6 @@ input[type=checkbox] {
|
||||
|
||||
.lesson-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
color: var(--gray-900);
|
||||
font-size: var(--text-base);
|
||||
@@ -544,10 +544,10 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.quiz-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.question {
|
||||
@@ -1366,7 +1366,7 @@ pre {
|
||||
.question-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.question-number {
|
||||
@@ -2027,3 +2027,18 @@ select {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.answer-indicator {
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 0.2rem 0.5rem;
|
||||
width: fit-content;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.answer-indicator.success {
|
||||
background-color: var(--dark-green-50);
|
||||
}
|
||||
|
||||
.answer-indicator.failure {
|
||||
background-color: var(--red-50);
|
||||
}
|
||||
@@ -46,9 +46,9 @@
|
||||
<form id="quiz-form" class="hide">
|
||||
<div class="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 }}">
|
||||
<div class="question-header">
|
||||
<div class="question-number">{{ loop.index }}. </div>
|
||||
@@ -58,6 +58,7 @@
|
||||
<div class="small"> {{ instruction }} </div>
|
||||
</div>
|
||||
|
||||
{% if question.type == "Choices" %}
|
||||
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
||||
{% for option in options %}
|
||||
{% if option %}
|
||||
@@ -66,8 +67,7 @@
|
||||
<label class="quiz-label">
|
||||
<div class="course-meta font-weight-bold"> {{ convert_number_to_character(loop.index - 1) }}</div>
|
||||
<input class="option" value="{{ option | urlencode }}"
|
||||
data-correct="{{ question['is_correct_' + loop.index | string] }}" {% if question.multiple %}
|
||||
type="checkbox" {% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
|
||||
{% if question.multiple %} type="checkbox" {% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
|
||||
<div class="option-text">{{ frappe.utils.md_to_html(option) }}</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -80,6 +80,14 @@
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -96,19 +104,19 @@
|
||||
{% endif %}
|
||||
|
||||
|
||||
<button class="button pull-right is-default" id="check" disabled>
|
||||
<button class="btn btn-secondary btn-sm pull-right" id="check" disabled>
|
||||
{{ _("Check") }}
|
||||
</button>
|
||||
<div class="button is-secondary hide" id="next">
|
||||
<div class="btn btn-secondary btn-sm hide" id="next">
|
||||
{{ _("Next Question") }}
|
||||
</div>
|
||||
<div class="button is-secondary is-default hide" id="summary">
|
||||
<div class="btn btn-secondary btn-sm hide" id="summary">
|
||||
{{ _("Submit") }}
|
||||
</div>
|
||||
<small id="submission-message" class="font-weight-bold hide">
|
||||
{{ _("Please join the course to submit the Quiz.") }}
|
||||
</small>
|
||||
<div class="button is-secondary hide" id="try-again">
|
||||
<div class="btn btn-secondary btn-sm hide" id="try-again">
|
||||
{{ _("Try Again") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ frappe.ready(() => {
|
||||
this.marked_as_complete = false;
|
||||
this.quiz_submitted = false;
|
||||
this.file_type;
|
||||
this.answer = [];
|
||||
this.is_correct = [];
|
||||
let self = this;
|
||||
|
||||
localStorage.removeItem($("#quiz-title").data("name"));
|
||||
@@ -16,6 +18,10 @@ frappe.ready(() => {
|
||||
enable_check(e);
|
||||
});
|
||||
|
||||
$(".possibility").keyup((e) => {
|
||||
enable_check(e);
|
||||
});
|
||||
|
||||
$(window).scroll(() => {
|
||||
let self = this;
|
||||
if (
|
||||
@@ -29,6 +35,7 @@ frappe.ready(() => {
|
||||
});
|
||||
|
||||
$("#summary").click((e) => {
|
||||
add_to_local_storage();
|
||||
quiz_summary(e);
|
||||
});
|
||||
|
||||
@@ -37,6 +44,7 @@ frappe.ready(() => {
|
||||
});
|
||||
|
||||
$("#next").click((e) => {
|
||||
add_to_local_storage();
|
||||
mark_active_question(e);
|
||||
});
|
||||
|
||||
@@ -224,29 +232,74 @@ const check_answer = (e = undefined) => {
|
||||
} else {
|
||||
$("#next").removeClass("hide");
|
||||
}
|
||||
let [answer, is_correct] = parse_options();
|
||||
add_to_local_storage(current_index, answer, is_correct);
|
||||
parse_options();
|
||||
};
|
||||
|
||||
const parse_options = () => {
|
||||
let answer = [];
|
||||
let is_correct = [];
|
||||
let type = $(".active-question").data("type");
|
||||
|
||||
$(".active-question input").each((i, element) => {
|
||||
let correct = parseInt($(element).attr("data-correct"));
|
||||
if ($(element).prop("checked")) {
|
||||
answer.push(decodeURIComponent($(element).val()));
|
||||
correct && is_correct.push(1);
|
||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
||||
} else {
|
||||
correct && is_correct.push(0);
|
||||
correct
|
||||
? add_icon(element, "minus-circle-green")
|
||||
: add_icon(element, "minus-circle");
|
||||
}
|
||||
if (type == "Choices") {
|
||||
$(".active-question input").each((i, element) => {
|
||||
is_answer_correct(type, element);
|
||||
});
|
||||
} else {
|
||||
is_answer_correct(type, $(".active-question textarea"));
|
||||
}
|
||||
};
|
||||
|
||||
const is_answer_correct = (type, element) => {
|
||||
let answer = 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(element, data.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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 = (element, correct) => {
|
||||
self.answer.push(decodeURIComponent($(element).val()));
|
||||
if (correct) {
|
||||
self.is_correct.push(1);
|
||||
show_indicator("success", element);
|
||||
} else {
|
||||
self.is_correct.push(0);
|
||||
show_indicator("failure", element);
|
||||
}
|
||||
};
|
||||
|
||||
const show_indicator = (class_name, element) => {
|
||||
let label = class_name == "success" ? "Correct" : "Incorrect";
|
||||
let icon =
|
||||
class_name == "success" ? "#icon-solid-success" : "#icon-solid-error";
|
||||
$(`<div class="answer-indicator ${class_name}">
|
||||
<svg class="icon icon-md">
|
||||
<use href=${icon}>
|
||||
</svg>
|
||||
<span style="font-weight: 500">${__(label)}</span>
|
||||
</div>`).insertAfter(element);
|
||||
};
|
||||
|
||||
const add_icon = (element, icon) => {
|
||||
@@ -258,21 +311,24 @@ const add_icon = (element, icon) => {
|
||||
${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_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
|
||||
let quiz_obj = {
|
||||
question_index: current_index,
|
||||
answer: answer.join(),
|
||||
is_correct: is_correct,
|
||||
answer: self.answer.join(),
|
||||
is_correct: self.is_correct,
|
||||
};
|
||||
|
||||
quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]);
|
||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored));
|
||||
|
||||
self.answer = [];
|
||||
self.is_correct = [];
|
||||
};
|
||||
|
||||
const create_certificate = (e) => {
|
||||
|
||||
@@ -37,12 +37,19 @@
|
||||
<div contenteditable="true" data-placeholder="{{ _('Question') }}" data-question="{{ question.name }}"
|
||||
class="question mb-4">{% if question.question %} {{ question.question }} {% endif %}</div>
|
||||
|
||||
<select value="{{ question.type }}" class="input-with-feedback form-control ellipsis type" maxlength="140" data-fieldtype="Select" data-fieldname="type" placeholder="" data-doctype="LMS Quiz Question">
|
||||
{% for option in ["Choices", "User Input"] %}
|
||||
<option value="{{ option }}" {% if question.type == option %} selected {% endif %} > {{ _(option) }} </option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% for i in range(1,5) %}
|
||||
{% set num = frappe.utils.cstr(i) %}
|
||||
|
||||
{% set option = question["option_" + num] %}
|
||||
{% set explanation = question["explanation_" + num] %}
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="option-group mt-4 {% if question.type == 'User Input' %} hide {% endif %} ">
|
||||
<label class=""> {{ _("Option") }} {{ num }} </label>
|
||||
<div class="d-flex justify-content-between option-{{ num }}">
|
||||
<div contenteditable="true" data-placeholder="{{ _('Option') }}"
|
||||
@@ -57,6 +64,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set possible_answer = question["possibility_" + num] %}
|
||||
|
||||
<div class="possibility-group mt-4 {% if question.type == 'Choices' %} hide {% endif %}">
|
||||
<label class=""> {{ _("Possible Answer") }} {{ num }} </label>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<div contenteditable="true" class="input-with-feedback form-control bold possibility-{{ num }}" style="height: 100px;" spellcheck="false">{% if possible_answer %}{{possible_answer}}{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -15,15 +15,25 @@ frappe.ready(() => {
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
|
||||
});
|
||||
|
||||
$(document).on("change", ".type", function () {
|
||||
toggle_form($(this));
|
||||
});
|
||||
|
||||
get_questions();
|
||||
});
|
||||
|
||||
const add_question = () => {
|
||||
/* if ($(".new-quiz-card").length) {
|
||||
scroll_to_question_container();
|
||||
return;
|
||||
} */
|
||||
const toggle_form = (el) => {
|
||||
let type = el.val();
|
||||
if (type === "Choices") {
|
||||
el.siblings(".option-group").removeClass("hide");
|
||||
el.siblings(".possibility-group").addClass("hide");
|
||||
} else if (type === "User Input") {
|
||||
el.siblings(".option-group").addClass("hide");
|
||||
el.siblings(".possibility-group").removeClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
const add_question = () => {
|
||||
let add_after = $(".quiz-card").length
|
||||
? $(".quiz-card:last")
|
||||
: $("#quiz-title");
|
||||
@@ -31,7 +41,11 @@ const add_question = () => {
|
||||
<div contenteditable="true" data-placeholder="${__(
|
||||
"Question"
|
||||
)}" class="question mb-4"></div>
|
||||
</div>`;
|
||||
<select value="{{ question.type }}" class="input-with-feedback form-control ellipsis type" maxlength="140" data-fieldtype="Select" data-fieldname="type" placeholder="" data-doctype="LMS Quiz Question">
|
||||
<option value="Choices"> ${__("Choices")} </option>
|
||||
<option value="User Input"> ${__("User Input")} </option>
|
||||
</select>
|
||||
</div>`;
|
||||
$(question_template).insertAfter(add_after);
|
||||
get_question_template();
|
||||
$(".btn-save-question").removeClass("hide");
|
||||
@@ -40,11 +54,31 @@ const add_question = () => {
|
||||
const get_question_template = () => {
|
||||
Array.from({ length: 4 }, (x, num) => {
|
||||
let option_template = get_option_template(num + 1);
|
||||
|
||||
let add_after = $(".quiz-card:last .option-group").length
|
||||
? $(".quiz-card:last .option-group").last()
|
||||
: $(".question:last");
|
||||
: $(".type:last");
|
||||
question_template = $(option_template).insertAfter(add_after);
|
||||
});
|
||||
|
||||
Array.from({ length: 4 }, (x, num) => {
|
||||
let possibility_template = get_possibility_template(num + 1);
|
||||
let add_after = $(".quiz-card:last .possibility-group").length
|
||||
? $(".quiz-card:last .possibility-group").last()
|
||||
: $(".quiz-card:last .option-group:last");
|
||||
question_template = $(possibility_template).insertAfter(add_after);
|
||||
});
|
||||
};
|
||||
|
||||
const get_possibility_template = (num) => {
|
||||
return `<div class="possibility-group mt-4 hide">
|
||||
<label class=""> ${__("Possible Answer")} ${num} </label>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<div contenteditable="true" class="input-with-feedback form-control bold possibility-{{ num }}" style="height: 100px;" spellcheck="false"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const get_option_template = (num) => {
|
||||
@@ -93,36 +127,42 @@ const get_questions = () => {
|
||||
|
||||
let details = {};
|
||||
let correct_options = 0;
|
||||
let possibilities = 0;
|
||||
|
||||
details["element"] = el;
|
||||
details["question"] = $(el).find(".question").text();
|
||||
details["question_name"] =
|
||||
$(el).find(".question").data("question") || "";
|
||||
details["type"] = $(el).find(".type").val();
|
||||
|
||||
Array.from({ length: 4 }, (x, i) => {
|
||||
let num = i + 1;
|
||||
|
||||
details[`option_${num}`] = $(el)
|
||||
.find(`.option-${num} .option-input:first`)
|
||||
.text();
|
||||
details[`explanation_${num}`] = $(el)
|
||||
.find(`.option-${num} .option-input:last`)
|
||||
.text();
|
||||
if (details.type == "Choices") {
|
||||
details[`option_${num}`] = $(el)
|
||||
.find(`.option-${num} .option-input:first`)
|
||||
.text();
|
||||
details[`explanation_${num}`] = $(el)
|
||||
.find(`.option-${num} .option-input:last`)
|
||||
.text();
|
||||
|
||||
let is_correct = $(el)
|
||||
.find(`.option-${num} .option-checkbox`)
|
||||
.find("input")
|
||||
.prop("checked");
|
||||
if (is_correct) correct_options += 1;
|
||||
let is_correct = $(el)
|
||||
.find(`.option-${num} .option-checkbox`)
|
||||
.find("input")
|
||||
.prop("checked");
|
||||
if (is_correct) correct_options += 1;
|
||||
|
||||
details[`is_correct_${num}`] = is_correct;
|
||||
details[`is_correct_${num}`] = is_correct;
|
||||
} else {
|
||||
let possible_answer = $(el)
|
||||
.find(`.possibility-${num}`)
|
||||
.text()
|
||||
.trim();
|
||||
if (possible_answer) possibilities += 1;
|
||||
details[`possibility_${num}`] = possible_answer;
|
||||
}
|
||||
});
|
||||
|
||||
if (!details["option_1"] || !details["option_2"])
|
||||
frappe.throw(__("Each question must have at least two options."));
|
||||
|
||||
if (!correct_options)
|
||||
frappe.throw(
|
||||
__("Each question must have at least one correct option.")
|
||||
);
|
||||
validate_mandatory(details, correct_options, possibilities);
|
||||
|
||||
details["multiple"] = correct_options > 1 ? 1 : 0;
|
||||
questions.push(details);
|
||||
@@ -131,12 +171,42 @@ const get_questions = () => {
|
||||
return questions;
|
||||
};
|
||||
|
||||
const validate_mandatory = (details, correct_options, possibilities) => {
|
||||
if (details["type"] == "Choices") {
|
||||
if (!details["option_1"] || !details["option_2"]) {
|
||||
scroll_to_element(details["element"]);
|
||||
frappe.throw(__("Each question must have at least two options."));
|
||||
}
|
||||
|
||||
if (!correct_options) {
|
||||
scroll_to_element(details["element"]);
|
||||
frappe.throw(
|
||||
__(
|
||||
"Question with choices must have at least one correct option."
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (!possibilities) {
|
||||
scroll_to_element(details["element"]);
|
||||
frappe.throw(
|
||||
__(
|
||||
"Question with user input must have at least one possible answer."
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const scroll_to_question_container = () => {
|
||||
$([document.documentElement, document.body]).animate(
|
||||
{
|
||||
scrollTop: $(".new-quiz-card").offset().top,
|
||||
},
|
||||
1000
|
||||
);
|
||||
scroll_to_element(".new-quiz-card:last");
|
||||
$(".new-quiz-card").find(".question").focus();
|
||||
};
|
||||
|
||||
const scroll_to_element = (element) => {
|
||||
if ($(element).length)
|
||||
$([document.documentElement, document.body]).animate(
|
||||
{
|
||||
scrollTop: $(element).offset().top,
|
||||
},
|
||||
1000
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,11 +9,12 @@ def get_context(context):
|
||||
context.quiz = frappe._dict()
|
||||
context.quiz.edit_mode = 1
|
||||
else:
|
||||
fields_arr = ["name", "question"]
|
||||
fields_arr = ["name", "question", "type"]
|
||||
for num in range(1, 5):
|
||||
fields_arr.append("option_" + cstr(num))
|
||||
fields_arr.append("is_correct_" + cstr(num))
|
||||
fields_arr.append("explanation_" + cstr(num))
|
||||
fields_arr.append("possibility_" + cstr(num))
|
||||
|
||||
context.quiz = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1)
|
||||
context.quiz.questions = frappe.get_all(
|
||||
|
||||
Reference in New Issue
Block a user