Merge branch 'main' of https://github.com/frappe/lms
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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