Merge pull request #540 from pateljannat/quiz-modal
fix: quiz enhancements
This commit is contained in:
@@ -62,7 +62,7 @@ def delete_lms_roles():
|
||||
|
||||
|
||||
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():
|
||||
|
||||
@@ -17,52 +17,7 @@ class LMSQuiz(Document):
|
||||
self.name = generate_slug(self.title, "LMS Quiz")
|
||||
|
||||
def validate(self):
|
||||
self.validate_correct_answers()
|
||||
|
||||
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))
|
||||
validate_correct_answers(self.questions)
|
||||
|
||||
def get_last_submission_details(self):
|
||||
"""Returns the latest submission for this user."""
|
||||
@@ -82,6 +37,71 @@ class LMSQuiz(Document):
|
||||
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):
|
||||
if doc.quiz_id:
|
||||
frappe.db.set_value(
|
||||
@@ -121,37 +141,84 @@ def quiz_summary(quiz, results):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_quiz(quiz_title, questions, quiz):
|
||||
def save_quiz(quiz_title, quiz):
|
||||
if quiz:
|
||||
doc = frappe.get_doc("LMS Quiz", quiz)
|
||||
frappe.db.set_value("LMS Quiz", quiz, "title", quiz_title)
|
||||
return quiz
|
||||
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})
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
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(
|
||||
for num in range(1, 5):
|
||||
if values.get(f"option_{num}"):
|
||||
doc.update(
|
||||
{
|
||||
"doctype": "LMS Quiz Question",
|
||||
"parent": doc.name,
|
||||
"parenttype": "LMS Quiz",
|
||||
"parentfield": "questions",
|
||||
"idx": index + 1,
|
||||
f"option_{num}": values[f"option_{num}"],
|
||||
f"is_correct_{num}": values[f"is_correct_{num}"],
|
||||
}
|
||||
)
|
||||
|
||||
question_doc.update(row)
|
||||
question_doc.save(ignore_permissions=True)
|
||||
if values.get(f"explanation_{num}"):
|
||||
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()
|
||||
|
||||
@@ -22,9 +22,8 @@ def execute():
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value(
|
||||
frappe.db.set_single_value(
|
||||
"LMS Settings",
|
||||
None,
|
||||
"mentor_request_creation",
|
||||
_("Mentor Request Creation Template"),
|
||||
)
|
||||
@@ -43,9 +42,8 @@ def execute():
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value(
|
||||
frappe.db.set_single_value(
|
||||
"LMS Settings",
|
||||
None,
|
||||
"mentor_request_status_update",
|
||||
_("Mentor Request Status Update Template"),
|
||||
)
|
||||
|
||||
@@ -162,8 +162,8 @@ textarea.field-input {
|
||||
|
||||
.lesson-editor {
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding-top: 0.5rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.lesson-parent .breadcrumb {
|
||||
@@ -185,12 +185,24 @@ textarea.field-input {
|
||||
.clickable {
|
||||
color: var(--gray-900);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
color: var(--gray-900);
|
||||
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 {
|
||||
@@ -471,10 +483,6 @@ input[type=checkbox] {
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.custom-checkbox>label>input {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.custom-checkbox>label>.empty-checkbox {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
@@ -493,16 +501,30 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.quiz-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.quiz-label p {
|
||||
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 {
|
||||
width: 50%;
|
||||
margin-bottom: 2rem;
|
||||
@@ -1056,7 +1078,7 @@ pre {
|
||||
|
||||
.column-card {
|
||||
flex-direction: column;
|
||||
padding: 1.25rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1502,41 +1524,21 @@ pre {
|
||||
}
|
||||
|
||||
.reviews-parent .progress-bar {
|
||||
background-color: var(--primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.course-home-top-container {
|
||||
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 {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--gray-900);
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin: 0 1rem;
|
||||
margin: 0.5rem 0 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.question-text .ql-editor.read-mode {
|
||||
white-space: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-column-grid {
|
||||
|
||||
@@ -9,20 +9,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="">
|
||||
<div id="start-banner">
|
||||
<button class="btn btn-secondary btn-sm btn-start-quiz pull-right">
|
||||
{{ _("Start the Quiz") }}
|
||||
</button>
|
||||
<div id="start-banner" class="common-card-style column-card align-items-center">
|
||||
|
||||
<h2 class="mt-3" id="quiz-title" data-name="{{ quiz.name }}" data-max-attempts="{{ quiz.max_attempts }}">
|
||||
{{ quiz.title }}
|
||||
</h2>
|
||||
<div class="text-center my-10">
|
||||
<div class="bold-heading" id="quiz-title" data-name="{{ quiz.name }}" data-max-attempts="{{ quiz.max_attempts }}">
|
||||
{{ quiz.title }}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info medium">
|
||||
<div class="">
|
||||
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
|
||||
</div>
|
||||
|
||||
@@ -40,22 +36,27 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-secondary btn-sm btn-start-quiz mt-4">
|
||||
{{ _("Start the Quiz") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<form id="quiz-form" class="hide">
|
||||
<form id="quiz-form" class="common-card-style column-card hide">
|
||||
<div class="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 }}"
|
||||
data-question="{{ question.question }}" data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
||||
<div class="question-header">
|
||||
<div class="question-number">{{ loop.index }}. </div>
|
||||
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
||||
<div>
|
||||
<div class="question-number">
|
||||
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
|
||||
<div class="question-text">
|
||||
{{ frappe.utils.md_to_html(question.question) }}
|
||||
{{ question.question }}
|
||||
</div>
|
||||
<div class="small"> {{ instruction }} </div>
|
||||
</div>
|
||||
|
||||
{% if question.type == "Choices" %}
|
||||
@@ -64,8 +65,7 @@
|
||||
{% if option %}
|
||||
<div class="mb-2">
|
||||
<div class="custom-checkbox">
|
||||
<label class="quiz-label">
|
||||
<div class="course-meta font-weight-bold"> {{ convert_number_to_character(loop.index - 1) }}</div>
|
||||
<label class="option-row">
|
||||
<input class="option" value="{{ option | urlencode }}"
|
||||
{% if question.multiple %} type="checkbox" {% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
|
||||
<div class="option-text">{{ frappe.utils.md_to_html(option) }}</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
{% 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>
|
||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control bold possibility mt-4" style="height: 150px;" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -93,7 +93,12 @@
|
||||
</div>
|
||||
|
||||
<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 %}
|
||||
<div class="progress timer w-75" data-time="{{ quiz.time }}">
|
||||
|
||||
@@ -12,7 +12,7 @@ frappe.ready(() => {
|
||||
save_current_lesson();
|
||||
|
||||
$(".option").click((e) => {
|
||||
enable_check(e);
|
||||
if (!$("#check").hasClass("hide")) enable_check(e);
|
||||
});
|
||||
|
||||
$(".possibility").keyup((e) => {
|
||||
@@ -286,6 +286,7 @@ const show_indicator = (class_name, element) => {
|
||||
|
||||
const add_icon = (element, icon) => {
|
||||
$(element).closest(".custom-checkbox").removeClass("active-option");
|
||||
$(element).closest(".option").addClass("hide");
|
||||
let label = $(element).siblings(".option-text").text();
|
||||
$(element).siblings(".option-text").html(`
|
||||
<div>
|
||||
|
||||
@@ -14,19 +14,31 @@
|
||||
{% endblock %}
|
||||
|
||||
{% macro QuizForm(quiz) %}
|
||||
<div>
|
||||
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
||||
{{ QuizDetails(quiz) }}
|
||||
{% if quiz.questions %}
|
||||
{% for question in quiz.questions %}
|
||||
{{ Question(question, loop.index) }}
|
||||
{% endfor %}
|
||||
<div class="field-group">
|
||||
<div class="field-label mb-1">
|
||||
{{ _("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 %}
|
||||
<div id="question-template" class="hide">
|
||||
{{ Question({}, 0) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="container form-width">
|
||||
@@ -40,18 +52,19 @@
|
||||
{{ _("Quiz List") }}
|
||||
</a>
|
||||
<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>
|
||||
|
||||
{% if quiz.name %}
|
||||
<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") }}
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm btn-save-question">
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,11 +79,11 @@
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Give your quiz a title") }}
|
||||
{{ _("Add a title for the quiz") }}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -78,68 +91,37 @@
|
||||
|
||||
{% macro Question(question, index) %}
|
||||
{% 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="field-group">
|
||||
<div>
|
||||
<div class="field-label question-label">
|
||||
{{ _("Question") }} {{ index }}
|
||||
</div>
|
||||
</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 class="list-row question-row" role="button" data-question="{{ question.name }}">
|
||||
<div class="flex clickable">
|
||||
<span class="mr-1">
|
||||
{{ index }}.
|
||||
</span>
|
||||
{{ question.question.split("\n")[0] }}
|
||||
</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 %}
|
||||
@@ -1,80 +1,148 @@
|
||||
frappe.ready(() => {
|
||||
if ($(".question-card").length <= 1) {
|
||||
add_question();
|
||||
}
|
||||
$("#quiz-title").focusout((e) => {
|
||||
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) => {
|
||||
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) => {
|
||||
if ($(el).hasClass("active")) {
|
||||
let type = $(el).find("input").data("type");
|
||||
if (type == "Choices") {
|
||||
$(el)
|
||||
.closest(".field-parent")
|
||||
.find(".options-group")
|
||||
.removeClass("hide");
|
||||
$(el)
|
||||
.closest(".field-parent")
|
||||
.find(".answers-group")
|
||||
.addClass("hide");
|
||||
} else {
|
||||
$(el)
|
||||
.closest(".field-parent")
|
||||
.find(".options-group")
|
||||
.addClass("hide");
|
||||
$(el)
|
||||
.closest(".field-parent")
|
||||
.find(".answers-group")
|
||||
.removeClass("hide");
|
||||
}
|
||||
}
|
||||
const show_quiz_modal = () => {
|
||||
let quiz_dialog = new frappe.ui.Dialog({
|
||||
title: __("Create Quiz"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Quiz Title"),
|
||||
fieldname: "quiz_title",
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
primary_action: (values) => {
|
||||
quiz_dialog.hide();
|
||||
save_quiz(values);
|
||||
},
|
||||
});
|
||||
|
||||
quiz_dialog.show();
|
||||
};
|
||||
|
||||
const add_question = (scroll = false) => {
|
||||
let template = $("#question-template").html();
|
||||
let index = $(".question-card:nth-last-child(2)").data("index") + 1 || 1;
|
||||
template = update_index(template, index);
|
||||
const show_question_modal = (values = {}) => {
|
||||
let fields = get_question_fields(values);
|
||||
|
||||
$(template).insertBefore($("#question-template"));
|
||||
scroll && scroll_to_question_container();
|
||||
this.question_dialog = new frappe.ui.Dialog({
|
||||
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 $template = $(template);
|
||||
$template.attr("data-index", index);
|
||||
$template.find(".question-label").text("Question " + index);
|
||||
$template.find(".question-type input").attr("name", "type-" + index);
|
||||
return $template.prop("outerHTML");
|
||||
const get_question_fields = (values = {}) => {
|
||||
let dialog_fields = [
|
||||
{
|
||||
fieldtype: "Text Editor",
|
||||
fieldname: "question",
|
||||
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) => {
|
||||
if (!$("#quiz-title").val()) {
|
||||
frappe.throw(__("Quiz Title is mandatory."));
|
||||
}
|
||||
const edit_question = (e) => {
|
||||
let question = $(e.currentTarget).data("question");
|
||||
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({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||
args: {
|
||||
quiz_title: $("#quiz-title").val(),
|
||||
questions: get_questions(),
|
||||
quiz: $("#quiz-title").data("name") || "",
|
||||
quiz_title: values.quiz_title,
|
||||
quiz: $("#quiz-form").data("name") || "",
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
@@ -88,90 +156,24 @@ const save_question = (e) => {
|
||||
});
|
||||
};
|
||||
|
||||
const get_questions = () => {
|
||||
let questions = [];
|
||||
const save_question = (values) => {
|
||||
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) => {
|
||||
if (!$(el).find(".question").val()) return;
|
||||
let details = {};
|
||||
let correct_options = 0;
|
||||
let possibilities = 0;
|
||||
|
||||
details["element"] = el;
|
||||
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);
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,11 +19,6 @@ def get_context(context):
|
||||
context.quiz = frappe._dict()
|
||||
else:
|
||||
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(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 _
|
||||
|
||||
|
||||
@@ -13,6 +13,5 @@ def get_context(context):
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
context.quiz_list = frappe.get_all(
|
||||
"LMS Quiz", {"owner": frappe.session.user}, ["name", "title"]
|
||||
)
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
context.quiz_list = frappe.get_all("LMS Quiz", filters, ["name", "title"])
|
||||
|
||||
Reference in New Issue
Block a user