feat: user input quiz portal form
This commit is contained in:
@@ -148,17 +148,7 @@ def save_quiz(quiz_title, questions, quiz):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
question_doc.update({"question": row["question"], "multiple": row["multiple"]})
|
question_doc.update(row)
|
||||||
|
|
||||||
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.save(ignore_permissions=True)
|
question_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
return doc.name
|
return doc.name
|
||||||
@@ -197,6 +187,8 @@ def check_input_answers(question, answer):
|
|||||||
"LMS Quiz Question", question, fields, as_dict=1
|
"LMS Quiz Question", question, fields, as_dict=1
|
||||||
)
|
)
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
if question_details[f"possibility_{num}"] == answer:
|
current_possibility = question_details[f"possibility_{num}"]
|
||||||
|
if current_possibility and current_possibility.lower() == answer.lower():
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ input[type=checkbox] {
|
|||||||
padding: 2rem 0 5rem;
|
padding: 2rem 0 5rem;
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
|
font-size: var(--text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.common-card-style {
|
.common-card-style {
|
||||||
@@ -425,7 +426,6 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.lesson-links {
|
.lesson-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
@@ -547,7 +547,7 @@ input[type=checkbox] {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 5rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question {
|
.question {
|
||||||
@@ -1366,7 +1366,7 @@ pre {
|
|||||||
.question-header {
|
.question-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-number {
|
.question-number {
|
||||||
@@ -2027,3 +2027,18 @@ select {
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
grid-gap: 1.5rem;
|
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);
|
||||||
|
}
|
||||||
@@ -104,19 +104,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<button class="button pull-right is-default" id="check" disabled>
|
<button class="btn btn-secondary btn-sm pull-right" id="check" disabled>
|
||||||
{{ _("Check") }}
|
{{ _("Check") }}
|
||||||
</button>
|
</button>
|
||||||
<div class="button is-secondary hide" id="next">
|
<div class="btn btn-secondary btn-sm hide" id="next">
|
||||||
{{ _("Next Question") }}
|
{{ _("Next Question") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="button is-secondary is-default hide" id="summary">
|
<div class="btn btn-secondary btn-sm hide" id="summary">
|
||||||
{{ _("Submit") }}
|
{{ _("Submit") }}
|
||||||
</div>
|
</div>
|
||||||
<small id="submission-message" class="font-weight-bold hide">
|
<small id="submission-message" class="font-weight-bold hide">
|
||||||
{{ _("Please join the course to submit the Quiz.") }}
|
{{ _("Please join the course to submit the Quiz.") }}
|
||||||
</small>
|
</small>
|
||||||
<div class="button is-secondary hide" id="try-again">
|
<div class="btn btn-secondary btn-sm hide" id="try-again">
|
||||||
{{ _("Try Again") }}
|
{{ _("Try Again") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ frappe.ready(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#summary").click((e) => {
|
$("#summary").click((e) => {
|
||||||
|
add_to_local_storage();
|
||||||
quiz_summary(e);
|
quiz_summary(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ const parse_options = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const is_answer_correct = (type, element) => {
|
const is_answer_correct = (type, element) => {
|
||||||
let answer = type == "Choices" ? decodeURIComponent($(element).val()) : "";
|
let answer = decodeURIComponent($(element).val());
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
async: false,
|
async: false,
|
||||||
@@ -260,7 +261,7 @@ const is_answer_correct = (type, element) => {
|
|||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
type == "Choices"
|
type == "Choices"
|
||||||
? parse_choices(element, data.message)
|
? parse_choices(element, data.message)
|
||||||
: parse_possible_answers(e);
|
: parse_possible_answers(element, data.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -278,9 +279,27 @@ const parse_choices = (element, correct) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parse_possible_answers = () => {
|
const parse_possible_answers = (element, correct) => {
|
||||||
self.answer.push(decodeURIComponent($(element).val()));
|
self.answer.push(decodeURIComponent($(element).val()));
|
||||||
correct ? self.is_correct.push(1) : self.is_correct.push(0);
|
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) => {
|
const add_icon = (element, icon) => {
|
||||||
@@ -292,7 +311,6 @@ const add_icon = (element, icon) => {
|
|||||||
${label}
|
${label}
|
||||||
</div>
|
</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 = () => {
|
const add_to_local_storage = () => {
|
||||||
@@ -308,6 +326,9 @@ const add_to_local_storage = () => {
|
|||||||
|
|
||||||
quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]);
|
quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]);
|
||||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored));
|
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored));
|
||||||
|
|
||||||
|
self.answer = [];
|
||||||
|
self.is_correct = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const create_certificate = (e) => {
|
const create_certificate = (e) => {
|
||||||
|
|||||||
@@ -37,8 +37,16 @@
|
|||||||
<div contenteditable="true" data-placeholder="{{ _('Question') }}" data-question="{{ question.name }}"
|
<div contenteditable="true" data-placeholder="{{ _('Question') }}" data-question="{{ question.name }}"
|
||||||
class="question mb-4">{% if question.question %} {{ question.question }} {% endif %}</div>
|
class="question mb-4">{% if question.question %} {{ question.question }} {% endif %}</div>
|
||||||
|
|
||||||
|
<select type="text" 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) %}
|
{% for i in range(1,5) %}
|
||||||
{% set num = frappe.utils.cstr(i) %}
|
{% set num = frappe.utils.cstr(i) %}
|
||||||
|
|
||||||
|
{% if question.type == "Choices" %}
|
||||||
{% set option = question["option_" + num] %}
|
{% set option = question["option_" + num] %}
|
||||||
{% set explanation = question["explanation_" + num] %}
|
{% set explanation = question["explanation_" + num] %}
|
||||||
|
|
||||||
@@ -57,6 +65,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
{% set possible_answer = question["possibility_" + num] %}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -93,13 +93,17 @@ const get_questions = () => {
|
|||||||
|
|
||||||
let details = {};
|
let details = {};
|
||||||
let correct_options = 0;
|
let correct_options = 0;
|
||||||
|
let possibilities = 0;
|
||||||
|
|
||||||
details["question"] = $(el).find(".question").text();
|
details["question"] = $(el).find(".question").text();
|
||||||
details["question_name"] =
|
details["question_name"] =
|
||||||
$(el).find(".question").data("question") || "";
|
$(el).find(".question").data("question") || "";
|
||||||
|
details["type"] = $(el).find(".type").val();
|
||||||
|
|
||||||
Array.from({ length: 4 }, (x, i) => {
|
Array.from({ length: 4 }, (x, i) => {
|
||||||
let num = i + 1;
|
let num = i + 1;
|
||||||
|
|
||||||
|
if (details.type == "Choices") {
|
||||||
details[`option_${num}`] = $(el)
|
details[`option_${num}`] = $(el)
|
||||||
.find(`.option-${num} .option-input:first`)
|
.find(`.option-${num} .option-input:first`)
|
||||||
.text();
|
.text();
|
||||||
@@ -114,15 +118,16 @@ const get_questions = () => {
|
|||||||
if (is_correct) correct_options += 1;
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
validate_mandatory(details, correct_options, possibilities);
|
||||||
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.")
|
|
||||||
);
|
|
||||||
|
|
||||||
details["multiple"] = correct_options > 1 ? 1 : 0;
|
details["multiple"] = correct_options > 1 ? 1 : 0;
|
||||||
questions.push(details);
|
questions.push(details);
|
||||||
@@ -131,6 +136,26 @@ const get_questions = () => {
|
|||||||
return questions;
|
return questions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validate_mandatory = (details, correct_options, possibilities) => {
|
||||||
|
if (details["type"] == "Choices") {
|
||||||
|
if (!details["option_1"] || !details["option_2"])
|
||||||
|
frappe.throw(__("Each question must have at least two options."));
|
||||||
|
|
||||||
|
if (!correct_options)
|
||||||
|
frappe.throw(
|
||||||
|
__(
|
||||||
|
"Question with choices must have at least one correct option."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (!possibilities) {
|
||||||
|
frappe.throw(
|
||||||
|
__(
|
||||||
|
"Question with user input must have at least one possible answer."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const scroll_to_question_container = () => {
|
const scroll_to_question_container = () => {
|
||||||
$([document.documentElement, document.body]).animate(
|
$([document.documentElement, document.body]).animate(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ def get_context(context):
|
|||||||
context.quiz = frappe._dict()
|
context.quiz = frappe._dict()
|
||||||
context.quiz.edit_mode = 1
|
context.quiz.edit_mode = 1
|
||||||
else:
|
else:
|
||||||
fields_arr = ["name", "question"]
|
fields_arr = ["name", "question", "type"]
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
fields_arr.append("option_" + cstr(num))
|
fields_arr.append("option_" + cstr(num))
|
||||||
fields_arr.append("is_correct_" + cstr(num))
|
fields_arr.append("is_correct_" + cstr(num))
|
||||||
fields_arr.append("explanation_" + 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 = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1)
|
||||||
context.quiz.questions = frappe.get_all(
|
context.quiz.questions = frappe.get_all(
|
||||||
|
|||||||
Reference in New Issue
Block a user