Merge pull request #540 from pateljannat/quiz-modal

fix: quiz enhancements
This commit is contained in:
Jannat Patel
2023-06-14 10:38:14 +05:30
committed by GitHub
10 changed files with 414 additions and 363 deletions

View File

@@ -62,7 +62,7 @@ def delete_lms_roles():
def set_default_home(): def set_default_home():
frappe.db.set_value("Portal Settings", None, "default_portal_home", "/courses") frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses")
def create_course_creator_role(): def create_course_creator_role():

View File

@@ -17,52 +17,7 @@ class LMSQuiz(Document):
self.name = generate_slug(self.title, "LMS Quiz") self.name = generate_slug(self.title, "LMS Quiz")
def validate(self): def validate(self):
self.validate_correct_answers() validate_correct_answers(self.questions)
def validate_correct_answers(self):
for question in self.questions:
if question.type == "Choices":
self.validate_correct_options(question)
else:
self.validate_possible_answer(question)
def validate_correct_options(self, question):
correct_options = self.get_correct_options(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 = [
"is_correct_1",
"is_correct_2",
"is_correct_3",
"is_correct_4",
]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def get_last_submission_details(self): def get_last_submission_details(self):
"""Returns the latest submission for this user.""" """Returns the latest submission for this user."""
@@ -82,6 +37,71 @@ class LMSQuiz(Document):
return result[0] return result[0]
def get_correct_options(question):
correct_option_fields = [
"is_correct_1",
"is_correct_2",
"is_correct_3",
"is_correct_4",
]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def validate_correct_answers(questions):
for question in questions:
if question.type == "Choices":
validate_duplicate_options(question)
validate_correct_options(question)
else:
validate_possible_answer(question)
def validate_duplicate_options(question):
options = []
for num in range(1, 5):
if question.get(f"option_{num}"):
options.append(question.get(f"option_{num}"))
if len(set(options)) != len(options):
frappe.throw(
_("Duplicate options found for this question: {0}").format(
frappe.bold(question.question)
)
)
def validate_correct_options(question):
correct_options = get_correct_options(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(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 update_lesson_info(doc, method): def update_lesson_info(doc, method):
if doc.quiz_id: if doc.quiz_id:
frappe.db.set_value( frappe.db.set_value(
@@ -121,37 +141,84 @@ def quiz_summary(quiz, results):
@frappe.whitelist() @frappe.whitelist()
def save_quiz(quiz_title, questions, quiz): def save_quiz(quiz_title, quiz):
if quiz: if quiz:
doc = frappe.get_doc("LMS Quiz", quiz) frappe.db.set_value("LMS Quiz", quiz, "title", quiz_title)
return quiz
else: else:
doc = frappe.get_doc( doc = frappe.new_doc("LMS Quiz")
doc.update({"title": quiz_title})
doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def save_question(quiz, values, index):
values = frappe._dict(json.loads(values))
validate_correct_answers([values])
if values.get("name"):
doc = frappe.get_doc("LMS Quiz Question", values.get("name"))
else:
doc = frappe.new_doc("LMS Quiz Question")
doc.update(
{
"question": values["question"],
"type": values["type"],
}
)
if not values.get("name"):
doc.update(
{ {
"doctype": "LMS Quiz", "parent": quiz,
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index,
} }
) )
doc.update({"title": quiz_title}) for num in range(1, 5):
doc.save(ignore_permissions=True) if values.get(f"option_{num}"):
doc.update(
for index, row in enumerate(json.loads(questions)):
if row["question_name"]:
question_doc = frappe.get_doc("LMS Quiz Question", row["question_name"])
else:
question_doc = frappe.get_doc(
{ {
"doctype": "LMS Quiz Question", f"option_{num}": values[f"option_{num}"],
"parent": doc.name, f"is_correct_{num}": values[f"is_correct_{num}"],
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index + 1,
} }
) )
question_doc.update(row) if values.get(f"explanation_{num}"):
question_doc.save(ignore_permissions=True) doc.update(
{
f"explanation_{num}": values[f"explanation_{num}"],
}
)
return doc.name if values.get(f"possibility_{num}"):
doc.update(
{
f"possibility_{num}": values[f"possibility_{num}"],
}
)
doc.save(ignore_permissions=True)
return quiz
@frappe.whitelist()
def get_question_details(question):
if frappe.db.exists("LMS Quiz Question", question):
fields = ["name", "question", "type"]
for num in range(1, 5):
fields.append(f"option_{cstr(num)}")
fields.append(f"is_correct_{cstr(num)}")
fields.append(f"explanation_{cstr(num)}")
fields.append(f"possibility_{cstr(num)}")
return frappe.db.get_value("LMS Quiz Question", question, fields, as_dict=1)
return
@frappe.whitelist() @frappe.whitelist()

View File

@@ -22,9 +22,8 @@ def execute():
} }
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
frappe.db.set_value( frappe.db.set_single_value(
"LMS Settings", "LMS Settings",
None,
"mentor_request_creation", "mentor_request_creation",
_("Mentor Request Creation Template"), _("Mentor Request Creation Template"),
) )
@@ -43,9 +42,8 @@ def execute():
} }
).insert(ignore_permissions=True) ).insert(ignore_permissions=True)
frappe.db.set_value( frappe.db.set_single_value(
"LMS Settings", "LMS Settings",
None,
"mentor_request_status_update", "mentor_request_status_update",
_("Mentor Request Status Update Template"), _("Mentor Request Status Update Template"),
) )

View File

@@ -162,8 +162,8 @@ textarea.field-input {
.lesson-editor { .lesson-editor {
border: 1px solid var(--gray-300); border: 1px solid var(--gray-300);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
padding-top: 0.5rem; padding-top: 0.5rem;
} }
.lesson-parent .breadcrumb { .lesson-parent .breadcrumb {
@@ -185,12 +185,24 @@ textarea.field-input {
.clickable { .clickable {
color: var(--gray-900); color: var(--gray-900);
font-weight: 500; font-weight: 500;
cursor: pointer;
} }
.clickable:hover { .clickable:hover {
color: var(--gray-900); color: var(--gray-900);
text-decoration: none; text-decoration: none;
cursor: pointer;
}
.question-row .ql-editor.read-mode p:hover {
cursor: pointer;
}
.question-row .ql-editor.read-mode p {
display: none;
}
.question-row .ql-editor.read-mode p:first-child {
display: block;
} }
.codex-editor path { .codex-editor path {
@@ -471,10 +483,6 @@ input[type=checkbox] {
color: var(--gray-700); color: var(--gray-700);
} }
.custom-checkbox>label>input {
visibility: hidden;
}
.custom-checkbox>label>.empty-checkbox { .custom-checkbox>label>.empty-checkbox {
height: 1.5rem; height: 1.5rem;
width: 1.5rem; width: 1.5rem;
@@ -493,16 +501,30 @@ input[type=checkbox] {
} }
.quiz-label { .quiz-label {
display: flex;
align-items: center;
margin-bottom: 0;
cursor: pointer;
} }
.quiz-label p { .quiz-label p {
display: inline; display: inline;
} }
.option-row {
display: flex;
align-items: center;
flex: 1;
margin-bottom: 0;
padding: 0.75rem;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-lg);
cursor:pointer;
background-color: var(--gray-100);
}
.active-option .option-row {
background-color: var(--blue-50);
border: 1px solid var(--blue-500);
}
.course-card-wide { .course-card-wide {
width: 50%; width: 50%;
margin-bottom: 2rem; margin-bottom: 2rem;
@@ -1056,7 +1078,7 @@ pre {
.column-card { .column-card {
flex-direction: column; flex-direction: column;
padding: 1.25rem; padding: 1rem;
height: 100%; height: 100%;
} }
@@ -1502,41 +1524,21 @@ pre {
} }
.reviews-parent .progress-bar { .reviews-parent .progress-bar {
background-color: var(--primary-color); background-color: var(--primary-color);
} }
.course-home-top-container { .course-home-top-container {
position: relative; position: relative;
} }
.question-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.question-number {
padding-right: 0.25rem;
}
.option-text {
padding: 0.75rem;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-md);
flex: 1;
}
.active-option .option-text {
background-color: var(--blue-50);
border: 1px solid var(--blue-500);
}
.question-text { .question-text {
font-size: var(--text-lg); margin: 0.5rem 0 1rem;
color: var(--gray-900); font-weight: 600;
font-weight: 600; }
flex: 1;
margin: 0 1rem; .question-text .ql-editor.read-mode {
white-space: inherit;
font-weight: 600;
} }
.profile-column-grid { .profile-column-grid {

View File

@@ -9,20 +9,16 @@
</div> </div>
</div> </div>
{% else %} {% else %}
<div class=""> <div class="">
<div id="start-banner"> <div id="start-banner" class="common-card-style column-card align-items-center">
<button class="btn btn-secondary btn-sm btn-start-quiz pull-right">
{{ _("Start the Quiz") }}
</button>
<h2 class="mt-3" id="quiz-title" data-name="{{ quiz.name }}" data-max-attempts="{{ quiz.max_attempts }}"> <div class="text-center my-10">
{{ quiz.title }} <div class="bold-heading" id="quiz-title" data-name="{{ quiz.name }}" data-max-attempts="{{ quiz.max_attempts }}">
</h2> {{ quiz.title }}
</div>
<div class="alert alert-info medium"> <div class="">
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
</div> </div>
@@ -40,22 +36,27 @@
</div> </div>
{% endif %} {% endif %}
<button class="btn btn-secondary btn-sm btn-start-quiz mt-4">
{{ _("Start the Quiz") }}
</button>
</div>
</div> </div>
<form id="quiz-form" class="hide"> <form id="quiz-form" class="common-card-style column-card 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.type == "Choices" and question.multiple else _("Choose 1 answer") if question.type == "Choices" else _("Enter the correct answer") %} {% set instruction = _("Choose all answers that apply") if question.type == "Choices" and question.multiple else _("Choose one answer") if question.type == "Choices" else _("Enter the correct answer") %}
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}" <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-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
<div class="question-header"> <div>
<div class="question-number">{{ loop.index }}. </div> <div class="question-number">
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
<div class="question-text"> <div class="question-text">
{{ frappe.utils.md_to_html(question.question) }} {{ question.question }}
</div> </div>
<div class="small"> {{ instruction }} </div>
</div> </div>
{% if question.type == "Choices" %} {% if question.type == "Choices" %}
@@ -64,8 +65,7 @@
{% if option %} {% if option %}
<div class="mb-2"> <div class="mb-2">
<div class="custom-checkbox"> <div class="custom-checkbox">
<label class="quiz-label"> <label class="option-row">
<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 }}"
{% 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> <div class="option-text">{{ frappe.utils.md_to_html(option) }}</div>
@@ -83,7 +83,7 @@
{% else %} {% else %}
<div class="control-input-wrapper"> <div class="control-input-wrapper">
<div class="control-input"> <div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control bold possibility" style="height: 150px;" spellcheck="false"></textarea> <textarea type="text" autocomplete="off" class="input-with-feedback form-control bold possibility mt-4" style="height: 150px;" spellcheck="false"></textarea>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -93,7 +93,12 @@
</div> </div>
<div class="quiz-footer"> <div class="quiz-footer">
<span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span> <span>
{{ _("Question") }}
<span class="current-question">1</span>
{{ _("of") }}
{{ quiz.questions | length }}
</span>
{% if quiz.time %} {% if quiz.time %}
<div class="progress timer w-75" data-time="{{ quiz.time }}"> <div class="progress timer w-75" data-time="{{ quiz.time }}">

View File

@@ -12,7 +12,7 @@ frappe.ready(() => {
save_current_lesson(); save_current_lesson();
$(".option").click((e) => { $(".option").click((e) => {
enable_check(e); if (!$("#check").hasClass("hide")) enable_check(e);
}); });
$(".possibility").keyup((e) => { $(".possibility").keyup((e) => {
@@ -286,6 +286,7 @@ const show_indicator = (class_name, element) => {
const add_icon = (element, icon) => { const add_icon = (element, icon) => {
$(element).closest(".custom-checkbox").removeClass("active-option"); $(element).closest(".custom-checkbox").removeClass("active-option");
$(element).closest(".option").addClass("hide");
let label = $(element).siblings(".option-text").text(); let label = $(element).siblings(".option-text").text();
$(element).siblings(".option-text").html(` $(element).siblings(".option-text").html(`
<div> <div>

View File

@@ -14,19 +14,31 @@
{% endblock %} {% endblock %}
{% macro QuizForm(quiz) %} {% macro QuizForm(quiz) %}
<div> <div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
{{ QuizDetails(quiz) }} {{ QuizDetails(quiz) }}
{% if quiz.questions %} {% if quiz.questions %}
{% for question in quiz.questions %} <div class="field-group">
{{ Question(question, loop.index) }} <div class="field-label mb-1">
{% endfor %} {{ _("Questions") }}
</div>
<div class="common-card-style column-card px-3 py-0">
{% for question in quiz.questions %}
{{ Question(question, loop.index) }}
{% endfor %}
</div>
<button class="btn btn-secondary btn-sm btn-add-question mt-4">
{{ _("Add Question") }}
</button>
</div>
{% endif %}
{% if quiz.name and not quiz.questions | length %}
{{ EmptyState() }}
{% endif %} {% endif %}
<div id="question-template" class="hide">
{{ Question({}, 0) }}
</div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro Header() %} {% macro Header() %}
<header class="sticky"> <header class="sticky">
<div class="container form-width"> <div class="container form-width">
@@ -40,18 +52,19 @@
{{ _("Quiz List") }} {{ _("Quiz List") }}
</a> </a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg"> <img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ quiz.title if quiz.title else _("New Quiz") }}</span> <span class="breadcrumb-destination">
{{ quiz.title if quiz.title else _("New Quiz") }}
</span>
</div> </div>
</div> </div>
{% if quiz.name %}
<div class="align-self-center"> <div class="align-self-center">
<button class="btn btn-default btn-sm btn-add-question"> <button class="btn btn-primary btn-sm btn-add-question">
{{ _("Add Question") }} {{ _("Add Question") }}
</button> </button>
<button class="btn btn-primary btn-sm btn-save-question">
{{ _("Save") }}
</button>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
@@ -66,11 +79,11 @@
{{ _("Title") }} {{ _("Title") }}
</div> </div>
<div class="field-description"> <div class="field-description">
{{ _("Give your quiz a title") }} {{ _("Add a title for the quiz") }}
</div> </div>
</div> </div>
<div class=""> <div class="">
<input type="text" class="field-input" id="quiz-title" {% if quiz.name %} value="{{ quiz.title }}" data-name="{{ quiz.name }}" {% endif %}> <input type="text" class="field-input" id="quiz-title" {% if quiz.name %} value="{{ quiz.title }}" data-title="{{ quiz.title }}" {% endif %}>
</div> </div>
</div> </div>
</div> </div>
@@ -78,68 +91,37 @@
{% macro Question(question, index) %} {% macro Question(question, index) %}
{% set type = question.type if question.type else "Choices" %} {% set type = question.type if question.type else "Choices" %}
<div class="common-card-style column-card field-parent question-card" data-index="{{ index }}"> <div class="list-row question-row" role="button" data-question="{{ question.name }}">
<div class="field-group"> <div class="flex clickable">
<div> <span class="mr-1">
<div class="field-label question-label"> {{ index }}.
{{ _("Question") }} {{ index }} </span>
</div> {{ question.question.split("\n")[0] }}
</div>
<div class="">
<input type="text" class="field-input question" {% if question.name %} value="{{ question.question }}" data-question="{{ question.name }}" {% endif %}>
</div>
</div>
<div class="field-group">
<div class="vertically-center justify-content-between">
<div class="field-label">
{{ _("Question Type") }}
</div>
<div class="btn-group btn-group-toggle type align-self-center" data-toggle="buttons">
<label class="btn btn-default btn-sm active question-type">
<input type="radio" name="type-{{ index }}" data-type="Choices" {% if type == "Choices" %} checked {% endif %}>
{{ _("Choices") }}
</label>
<label class="btn btn-default btn-sm question-type">
<input type="radio" name="type-{{ index }}" data-type="User Input" {% if type == "User Input" %} checked {% endif %}>
{{ _("User Input") }}
</label>
</div>
</div>
</div>
<div class="">
{% for i in range(1,5) %}
{% set num = frappe.utils.cstr(i) %}
{% set option = question["option_" + num] %}
{% set explanation = question["explanation_" + num] %}
{% set possible_answer = question["possibility_" + num] %}
<div class="field-group">
<div class="options-group {% if type == 'User Input' %} hide {% endif %}">
<textarea placeholder="Option" class="field-input option-{{ num }}"
style="height: 100px;">{% if option %}{{ option }}{% endif %}</textarea>
<input type="text" placeholder="Explanation" class="field-input explanation-{{ num }}" {% if explanation %} value="{{ explanation }}" {% endif %}>
<label class="vertically-center mt-1">
<input type="checkbox" class="correct-{{ num }}" {% if question['is_correct_' + num] %} checked {% endif %}>
{{ _("Is Correct") }}
</label>
</div>
<div class="answers-group {% if type == 'Choices' %} hide {% endif %}">
<div class="field-label">
{{ _("Possible Answers") }} {{ num }}
</div>
<textarea class="field-input possibility-{{ num }}"
style="height: 100px;">{% if possible_answer %}{{ possible_answer }}{% endif %}</textarea>
</div>
</div>
{% endfor %}
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro EmptyState() %}
<article class="empty-state my-5">
<div class="text-center">
<div class="bold-heading">
{{ _("You have not added any question yet") }}
</div>
<div>
{{ _("Create and manage questions from here.") }}
</div>
<div class="mt-4">
<button class="btn btn-default btn-sm btn-add-question">
<span>
{{ _("Add Question") }}
</span>
</button>
</div>
</div>
</article>
{% endmacro %}
{%- block script %}
{{ super() }}
{{ include_script('controls.bundle.js') }}
{% endblock %}

View File

@@ -1,80 +1,148 @@
frappe.ready(() => { frappe.ready(() => {
if ($(".question-card").length <= 1) { $("#quiz-title").focusout((e) => {
add_question(); if ($("#quiz-title").val() != $("#quiz-title").data("title")) {
} save_quiz({ quiz_title: $("#quiz-title").val() });
}
});
$(".question-row").click((e) => {
edit_question(e);
});
$(".btn-add-question").click((e) => { $(".btn-add-question").click((e) => {
add_question(true); show_question_modal();
}); });
$(".btn-save-question").click((e) => {
save_question(e);
});
$(".copy-quiz-id").click((e) => {
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
});
$(document).on("click", ".question-type", (e) => {
toggle_form($(e.currentTarget));
});
get_questions();
}); });
const toggle_form = (el) => { const show_quiz_modal = () => {
if ($(el).hasClass("active")) { let quiz_dialog = new frappe.ui.Dialog({
let type = $(el).find("input").data("type"); title: __("Create Quiz"),
if (type == "Choices") { fields: [
$(el) {
.closest(".field-parent") fieldtype: "Data",
.find(".options-group") label: __("Quiz Title"),
.removeClass("hide"); fieldname: "quiz_title",
$(el) reqd: 1,
.closest(".field-parent") },
.find(".answers-group") ],
.addClass("hide"); primary_action: (values) => {
} else { quiz_dialog.hide();
$(el) save_quiz(values);
.closest(".field-parent") },
.find(".options-group") });
.addClass("hide");
$(el) quiz_dialog.show();
.closest(".field-parent")
.find(".answers-group")
.removeClass("hide");
}
}
}; };
const add_question = (scroll = false) => { const show_question_modal = (values = {}) => {
let template = $("#question-template").html(); let fields = get_question_fields(values);
let index = $(".question-card:nth-last-child(2)").data("index") + 1 || 1;
template = update_index(template, index);
$(template).insertBefore($("#question-template")); this.question_dialog = new frappe.ui.Dialog({
scroll && scroll_to_question_container(); title: __("Add Question"),
fields: fields,
primary_action: (data) => {
if (values) data.name = values.name;
save_question(data);
},
});
question_dialog.show();
}; };
const update_index = (template, index) => { const get_question_fields = (values = {}) => {
const $template = $(template); let dialog_fields = [
$template.attr("data-index", index); {
$template.find(".question-label").text("Question " + index); fieldtype: "Text Editor",
$template.find(".question-type input").attr("name", "type-" + index); fieldname: "question",
return $template.prop("outerHTML"); label: __("Question"),
reqd: 1,
default: values.question || "",
},
{
fieldtype: "Select",
fieldname: "type",
label: __("Type"),
options: ["Choices", "User Input"],
default: values.type || "Choices",
},
];
Array.from({ length: 4 }, (x, i) => {
num = i + 1;
dialog_fields.push({
fieldtype: "Section Break",
fieldname: `section_break_${num}`,
});
let option = {
fieldtype: "Small Text",
fieldname: `option_${num}`,
label: __("Option") + ` ${num}`,
depends_on: "eval:doc.type=='Choices'",
default: values[`option_${num}`] || "",
};
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
dialog_fields.push(option);
dialog_fields.push({
fieldtype: "Data",
fieldname: `explanaion_${num}`,
label: __("Explanation"),
depends_on: "eval:doc.type=='Choices'",
default: values[`explanaion_${num}`] || "",
});
let is_correct = {
fieldtype: "Check",
fieldname: `is_correct_${num}`,
label: __("Is Correct"),
depends_on: "eval:doc.type=='Choices'",
default: values[`is_correct_${num}`] || 0,
};
if (num <= 2)
is_correct.mandatory_depends_on = "eval:doc.type=='Choices'";
dialog_fields.push(is_correct);
possibility = {
fieldtype: "Small Text",
fieldname: `possibility_${num}`,
label: __("Possible Answer") + ` ${num}`,
depends_on: "eval:doc.type=='User Input'",
default: values[`possibility_${num}`] || "",
};
if (num == 1)
possibility.mandatory_depends_on = "eval:doc.type=='User Input'";
dialog_fields.push(possibility);
});
return dialog_fields;
}; };
const save_question = (e) => { const edit_question = (e) => {
if (!$("#quiz-title").val()) { let question = $(e.currentTarget).data("question");
frappe.throw(__("Quiz Title is mandatory.")); frappe.call({
} method: "lms.lms.doctype.lms_quiz.lms_quiz.get_question_details",
args: {
question: question,
},
callback: (data) => {
if (data.message) show_question_modal(data.message);
},
});
};
const save_quiz = (values) => {
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz", method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: { args: {
quiz_title: $("#quiz-title").val(), quiz_title: values.quiz_title,
questions: get_questions(), quiz: $("#quiz-form").data("name") || "",
quiz: $("#quiz-title").data("name") || "",
}, },
callback: (data) => { callback: (data) => {
frappe.show_alert({ frappe.show_alert({
@@ -88,90 +156,24 @@ const save_question = (e) => {
}); });
}; };
const get_questions = () => { const save_question = (values) => {
let questions = []; frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_question",
args: {
quiz: $("#quiz-form").data("name") || "",
values: values,
index: $("#quiz-form").data("index") + 1,
},
callback: (data) => {
if (data.message) this.question_dialog.hide();
$(".field-parent").each((i, el) => { frappe.show_alert({
if (!$(el).find(".question").val()) return; message: __("Saved"),
let details = {}; indicator: "green",
let correct_options = 0; });
let possibilities = 0; setTimeout(() => {
window.location.reload();
details["element"] = el; }, 1000);
details["question"] = $(el).find(".question").val(); },
details["question_name"] =
$(el).find(".question").data("question") || "";
details["type"] = $(el).find("label.active").find("input").data("type");
Array.from({ length: 4 }, (x, i) => {
let num = i + 1;
if (details.type == "Choices") {
details[`option_${num}`] = $(el).find(`.option-${num}`).val();
details[`explanation_${num}`] = $(el)
.find(`.explanation-${num}`)
.val();
let is_correct = $(el).find(`.correct-${num}`).prop("checked");
if (is_correct) correct_options += 1;
details[`is_correct_${num}`] = is_correct;
} else {
let possible_answer = $(el)
.find(`.possibility-${num}`)
.val()
.trim();
if (possible_answer) possibilities += 1;
details[`possibility_${num}`] = possible_answer;
}
});
validate_mandatory(details, correct_options, possibilities);
details["multiple"] = correct_options > 1 ? 1 : 0;
questions.push(details);
}); });
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 = () => {
scroll_to_element(".question-card:nth-last-child(2)");
$(".question-card:nth-last-child(2)").find(".question").focus();
};
const scroll_to_element = (element) => {
if ($(element).length)
$([document.documentElement, document.body]).animate(
{
scrollTop: $(element).offset().top - 100,
},
1000
);
}; };

View File

@@ -19,11 +19,6 @@ def get_context(context):
context.quiz = frappe._dict() context.quiz = frappe._dict()
else: else:
fields_arr = ["name", "question", "type"] 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 = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1)
context.quiz.questions = frappe.get_all( context.quiz.questions = frappe.get_all(

View File

@@ -1,5 +1,5 @@
import frappe import frappe
from lms.lms.utils import can_create_courses from lms.lms.utils import can_create_courses, has_course_moderator_role
from frappe import _ from frappe import _
@@ -13,6 +13,5 @@ def get_context(context):
raise frappe.PermissionError(_(message)) raise frappe.PermissionError(_(message))
context.quiz_list = frappe.get_all( filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
"LMS Quiz", {"owner": frappe.session.user}, ["name", "title"] context.quiz_list = frappe.get_all("LMS Quiz", filters, ["name", "title"])
)