This commit is contained in:
Jannat Patel
2023-06-14 14:39:49 +05:30
11 changed files with 427 additions and 369 deletions

View File

@@ -13,7 +13,7 @@ frappe.ready(() => {
save_current_lesson();
$(".option").click((e) => {
enable_check(e);
if (!$("#check").hasClass("hide")) enable_check(e);
});
$(".possibility").keyup((e) => {
@@ -287,6 +287,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>

View File

@@ -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,67 +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 %}">
<input type="text" placeholder="Option" class="field-input option-{{ num }}" {% if option %} value="{{ option }}" {% endif %}>
<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 %}

View File

@@ -1,171 +1,179 @@
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) => {
window.location.href = `/quizzes/${data.message}`;
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.href = `/quizzes/${data.message}`;
}, 2000);
},
});
};
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
);
};

View File

@@ -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(

View File

@@ -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"])