feat: quiz in classes
This commit is contained in:
@@ -184,6 +184,10 @@ website_route_rules = [
|
||||
"from_route": "/assignment-submission/<assignment>/<submission>",
|
||||
"to_route": "assignment_submission/assignment_submission",
|
||||
},
|
||||
{
|
||||
"from_route": "/quiz-submission/<quiz>/<submission>",
|
||||
"to_route": "quiz_submission/quiz_submission",
|
||||
},
|
||||
]
|
||||
|
||||
website_redirects = [
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import hashlib
|
||||
import random
|
||||
import re
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
|
||||
@@ -16,6 +16,7 @@ be loaded in a webpage.
|
||||
|
||||
import frappe
|
||||
from urllib.parse import quote
|
||||
from frappe import _
|
||||
|
||||
|
||||
class PageExtension:
|
||||
@@ -102,8 +103,13 @@ def set_mandatory_fields_for_profile():
|
||||
|
||||
|
||||
def quiz_renderer(quiz_name):
|
||||
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
||||
if frappe.session.user == "Guest":
|
||||
return " <div class='alert alert-info'>" + _(
|
||||
"Quiz is not available to Guest users. Please login to continue."
|
||||
)
|
||||
+"</div>"
|
||||
|
||||
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
||||
context = {"quiz": quiz}
|
||||
|
||||
no_of_attempts = frappe.db.count(
|
||||
@@ -116,7 +122,7 @@ def quiz_renderer(quiz_name):
|
||||
)
|
||||
|
||||
context.update({"attempts_exceeded": True, "last_attempt_score": last_attempt_score})
|
||||
return frappe.render_template("templates/quiz.html", context)
|
||||
return frappe.render_template("templates/quiz/quiz.html", context)
|
||||
|
||||
|
||||
def exercise_renderer(argument):
|
||||
|
||||
@@ -1950,7 +1950,7 @@ select {
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: var(--text-base) !important;
|
||||
font-size: var(--text-lg) !important;
|
||||
}
|
||||
|
||||
.class-form-title {
|
||||
|
||||
@@ -70,7 +70,7 @@ const file_size = (value) => {
|
||||
|
||||
const join_course = (e) => {
|
||||
e.preventDefault();
|
||||
let course = $("#outline-heading").attr("data-course");
|
||||
let course = $(e.currentTarget).attr("data-course");
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||
return;
|
||||
|
||||
@@ -118,9 +118,6 @@
|
||||
<div class="btn btn-secondary btn-sm hide" id="summary">
|
||||
{{ _("Submit") }}
|
||||
</div>
|
||||
<small id="submission-message" class="font-weight-bold hide">
|
||||
{{ _("Please join the course to submit the Quiz.") }}
|
||||
</small>
|
||||
<div class="btn btn-secondary btn-sm hide" id="try-again">
|
||||
{{ _("Try Again") }}
|
||||
</div>
|
||||
262
lms/templates/quiz/quiz.js
Normal file
262
lms/templates/quiz/quiz.js
Normal file
@@ -0,0 +1,262 @@
|
||||
frappe.ready(() => {
|
||||
this.quiz_submitted = false;
|
||||
this.answer = [];
|
||||
this.is_correct = [];
|
||||
const self = this;
|
||||
|
||||
$(".btn-start-quiz").click((e) => {
|
||||
$("#start-banner").addClass("hide");
|
||||
$("#quiz-form").removeClass("hide");
|
||||
mark_active_question();
|
||||
});
|
||||
|
||||
$(".option").click((e) => {
|
||||
if (!$("#check").hasClass("hide")) enable_check(e);
|
||||
});
|
||||
|
||||
$(".possibility").keyup((e) => {
|
||||
enable_check(e);
|
||||
});
|
||||
|
||||
$("#summary").click((e) => {
|
||||
add_to_local_storage();
|
||||
quiz_summary(e);
|
||||
});
|
||||
|
||||
$("#check").click((e) => {
|
||||
check_answer(e);
|
||||
});
|
||||
|
||||
$("#next").click((e) => {
|
||||
add_to_local_storage();
|
||||
mark_active_question(e);
|
||||
});
|
||||
|
||||
$("#try-again").click((e) => {
|
||||
try_quiz_again(e);
|
||||
});
|
||||
|
||||
if ($("#quiz-title").data("max-attempts")) {
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.returnValue = "";
|
||||
if ($(".active-question").length && !self.quiz_submitted) {
|
||||
quiz_summary();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const mark_active_question = (e = undefined) => {
|
||||
$(".timer").addClass("hide");
|
||||
calculate_and_display_time(100);
|
||||
$(".timer").removeClass("hide");
|
||||
|
||||
let current_index = $(".active-question").attr("data-qt-index") || 0;
|
||||
let next_index = parseInt(current_index) + 1;
|
||||
|
||||
$(".question").addClass("hide").removeClass("active-question");
|
||||
$(`.question[data-qt-index='${next_index}']`)
|
||||
.removeClass("hide")
|
||||
.addClass("active-question");
|
||||
$(".current-question").text(`${next_index}`);
|
||||
$("#check").removeClass("hide").attr("disabled", true);
|
||||
$("#next").addClass("hide");
|
||||
$(".explanation").addClass("hide");
|
||||
initialize_timer();
|
||||
};
|
||||
|
||||
const calculate_and_display_time = (percent_time) => {
|
||||
$(".timer .progress-bar").attr("aria-valuenow", percent_time);
|
||||
$(".timer .progress-bar").attr("aria-valuemax", percent_time);
|
||||
$(".timer .progress-bar").css("width", `${percent_time}%`);
|
||||
let progress_color = percent_time < 20 ? "red" : "var(--primary-color)";
|
||||
$(".timer .progress-bar").css("background-color", progress_color);
|
||||
};
|
||||
|
||||
const initialize_timer = () => {
|
||||
this.time_left = $(".timer").data("time");
|
||||
calculate_and_display_time(100, this.time_left);
|
||||
$(".timer").removeClass("hide");
|
||||
const total_time = $(".timer").data("time");
|
||||
this.start_time = new Date().getTime();
|
||||
const self = this;
|
||||
let old_diff;
|
||||
|
||||
this.timer = setInterval(function () {
|
||||
var diff = (new Date().getTime() - self.start_time) / 1000;
|
||||
var variation = old_diff ? diff - old_diff : diff;
|
||||
old_diff = diff;
|
||||
self.time_left -= variation;
|
||||
let percent_time = (self.time_left / total_time) * 100;
|
||||
calculate_and_display_time(percent_time);
|
||||
if (self.time_left <= 0) {
|
||||
clearInterval(self.timer);
|
||||
$(".timer").addClass("hide");
|
||||
check_answer();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const enable_check = (e) => {
|
||||
if ($(".option:checked").length || $(".possibility").val().trim()) {
|
||||
$("#check").removeAttr("disabled");
|
||||
$(".custom-checkbox").removeClass("active-option");
|
||||
$(".option:checked")
|
||||
.closest(".custom-checkbox")
|
||||
.addClass("active-option");
|
||||
}
|
||||
};
|
||||
|
||||
const quiz_summary = (e = undefined) => {
|
||||
e && e.preventDefault();
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let total_questions = $(".question").length;
|
||||
let self = this;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
|
||||
args: {
|
||||
quiz: quiz_name,
|
||||
results: localStorage.getItem(quiz_name),
|
||||
},
|
||||
callback: (data) => {
|
||||
$(".question").addClass("hide");
|
||||
$("#summary").addClass("hide");
|
||||
$(".quiz-footer").prepend(
|
||||
`<div class="summary">
|
||||
<div class="font-weight-bold"> ${__("Score")}: ${
|
||||
data.message
|
||||
}/${total_questions} </div>
|
||||
</div>`
|
||||
);
|
||||
$("#try-again").removeClass("hide");
|
||||
self.quiz_submitted = true;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const try_quiz_again = (e) => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const check_answer = (e = undefined) => {
|
||||
e && e.preventDefault();
|
||||
|
||||
let answer = $(".active-question textarea");
|
||||
if (answer.length && !answer.val().trim()) {
|
||||
frappe.throw(__("Please enter your answer"));
|
||||
}
|
||||
|
||||
clearInterval(self.timer);
|
||||
$(".timer").addClass("hide");
|
||||
|
||||
let total_questions = $(".question").length;
|
||||
let current_index = $(".active-question").attr("data-qt-index");
|
||||
|
||||
$(".explanation").removeClass("hide");
|
||||
$("#check").addClass("hide");
|
||||
|
||||
if (current_index == total_questions) {
|
||||
$("#summary").removeClass("hide");
|
||||
} else {
|
||||
$("#next").removeClass("hide");
|
||||
}
|
||||
parse_options();
|
||||
};
|
||||
|
||||
const parse_options = () => {
|
||||
let type = $(".active-question").data("type");
|
||||
|
||||
if (type == "Choices") {
|
||||
$(".active-question input").each((i, element) => {
|
||||
is_answer_correct(type, element);
|
||||
});
|
||||
} else {
|
||||
is_answer_correct(type, $(".active-question textarea"));
|
||||
}
|
||||
};
|
||||
|
||||
const is_answer_correct = (type, element) => {
|
||||
let answer = decodeURIComponent($(element).val());
|
||||
|
||||
frappe.call({
|
||||
async: false,
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.check_answer",
|
||||
args: {
|
||||
question: $(".active-question").data("name"),
|
||||
type: type,
|
||||
answer: answer,
|
||||
},
|
||||
callback: (data) => {
|
||||
type == "Choices"
|
||||
? parse_choices(element, data.message)
|
||||
: parse_possible_answers(element, data.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const parse_choices = (element, correct) => {
|
||||
if ($(element).prop("checked")) {
|
||||
self.answer.push(decodeURIComponent($(element).val()));
|
||||
correct && self.is_correct.push(1);
|
||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
||||
} else {
|
||||
correct && self.is_correct.push(0);
|
||||
correct
|
||||
? add_icon(element, "minus-circle-green")
|
||||
: add_icon(element, "minus-circle");
|
||||
}
|
||||
};
|
||||
|
||||
const parse_possible_answers = (element, correct) => {
|
||||
self.answer.push(decodeURIComponent($(element).val()));
|
||||
if (correct) {
|
||||
self.is_correct.push(1);
|
||||
show_indicator("success", element);
|
||||
} else {
|
||||
self.is_correct.push(0);
|
||||
show_indicator("failure", element);
|
||||
}
|
||||
};
|
||||
|
||||
const show_indicator = (class_name, element) => {
|
||||
let label = class_name == "success" ? "Correct" : "Incorrect";
|
||||
let icon =
|
||||
class_name == "success" ? "#icon-solid-success" : "#icon-solid-error";
|
||||
$(`<div class="answer-indicator ${class_name}">
|
||||
<svg class="icon icon-md">
|
||||
<use href=${icon}>
|
||||
</svg>
|
||||
<span style="font-weight: 500">${__(label)}</span>
|
||||
</div>`).insertAfter(element);
|
||||
};
|
||||
|
||||
const add_icon = (element, icon) => {
|
||||
$(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>
|
||||
<img class="d-inline mr-3" src="/assets/lms/icons/${icon}.svg">
|
||||
${label}
|
||||
</div>
|
||||
`);
|
||||
};
|
||||
|
||||
const add_to_local_storage = () => {
|
||||
let current_index = $(".active-question").attr("data-qt-index");
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
|
||||
let quiz_obj = {
|
||||
question_index: current_index,
|
||||
answer: self.answer.join(),
|
||||
is_correct: self.is_correct,
|
||||
};
|
||||
|
||||
quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]);
|
||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored));
|
||||
|
||||
self.answer = [];
|
||||
self.is_correct = [];
|
||||
};
|
||||
@@ -5,6 +5,10 @@ from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
submission = frappe.form_dict["submission"]
|
||||
assignment = frappe.form_dict["assignment"]
|
||||
|
||||
@@ -87,8 +87,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="course-home-headings title {% if membership %} is-member {% endif %}
|
||||
{% if membership or is_instructor %} eligible-for-submission {% endif %}" id="title"
|
||||
<div class="course-home-headings title {% if membership %} is-member {% endif %}" id="title"
|
||||
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
|
||||
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
|
||||
>{% if lesson.title %}{{ lesson.title }}{% endif %}</div>
|
||||
@@ -246,6 +245,7 @@
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
{% include "lms/templates/quiz/quiz.js" %}
|
||||
</script>
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_footer() }}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
frappe.ready(() => {
|
||||
this.marked_as_complete = false;
|
||||
this.quiz_submitted = false;
|
||||
this.answer = [];
|
||||
this.is_correct = [];
|
||||
let self = this;
|
||||
|
||||
frappe.telemetry.capture("on_lesson_page", "lms");
|
||||
@@ -12,14 +9,6 @@ frappe.ready(() => {
|
||||
|
||||
save_current_lesson();
|
||||
|
||||
$(".option").click((e) => {
|
||||
if (!$("#check").hasClass("hide")) enable_check(e);
|
||||
});
|
||||
|
||||
$(".possibility").keyup((e) => {
|
||||
enable_check(e);
|
||||
});
|
||||
|
||||
$(window).scroll(() => {
|
||||
let self = this;
|
||||
if (
|
||||
@@ -32,24 +21,6 @@ frappe.ready(() => {
|
||||
}
|
||||
});
|
||||
|
||||
$("#summary").click((e) => {
|
||||
add_to_local_storage();
|
||||
quiz_summary(e);
|
||||
});
|
||||
|
||||
$("#check").click((e) => {
|
||||
check_answer(e);
|
||||
});
|
||||
|
||||
$("#next").click((e) => {
|
||||
add_to_local_storage();
|
||||
mark_active_question(e);
|
||||
});
|
||||
|
||||
$("#try-again").click((e) => {
|
||||
try_quiz_again(e);
|
||||
});
|
||||
|
||||
$("#certification").click((e) => {
|
||||
create_certificate(e);
|
||||
});
|
||||
@@ -62,12 +33,6 @@ frappe.ready(() => {
|
||||
clear_work(e);
|
||||
});
|
||||
|
||||
$(".btn-start-quiz").click((e) => {
|
||||
$("#start-banner").addClass("hide");
|
||||
$("#quiz-form").removeClass("hide");
|
||||
mark_active_question();
|
||||
});
|
||||
|
||||
$(".btn-back").click((e) => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
});
|
||||
@@ -76,15 +41,6 @@ frappe.ready(() => {
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
|
||||
$(".attachments").collapse("hide");
|
||||
});
|
||||
|
||||
if ($("#quiz-title").data("max-attempts")) {
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.returnValue = "";
|
||||
if ($(".active-question").length && !self.quiz_submitted) {
|
||||
quiz_summary();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const save_current_lesson = () => {
|
||||
@@ -96,35 +52,6 @@ const save_current_lesson = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const enable_check = (e) => {
|
||||
if ($(".option:checked").length || $(".possibility").val().trim()) {
|
||||
$("#check").removeAttr("disabled");
|
||||
$(".custom-checkbox").removeClass("active-option");
|
||||
$(".option:checked")
|
||||
.closest(".custom-checkbox")
|
||||
.addClass("active-option");
|
||||
}
|
||||
};
|
||||
|
||||
const mark_active_question = (e = undefined) => {
|
||||
$(".timer").addClass("hide");
|
||||
calculate_and_display_time(100);
|
||||
$(".timer").removeClass("hide");
|
||||
|
||||
let current_index = $(".active-question").attr("data-qt-index") || 0;
|
||||
let next_index = parseInt(current_index) + 1;
|
||||
|
||||
$(".question").addClass("hide").removeClass("active-question");
|
||||
$(`.question[data-qt-index='${next_index}']`)
|
||||
.removeClass("hide")
|
||||
.addClass("active-question");
|
||||
$(".current-question").text(`${next_index}`);
|
||||
$("#check").removeClass("hide").attr("disabled", true);
|
||||
$("#next").addClass("hide");
|
||||
$(".explanation").addClass("hide");
|
||||
initialize_timer();
|
||||
};
|
||||
|
||||
const mark_progress = () => {
|
||||
let status = "Complete";
|
||||
frappe.call({
|
||||
@@ -155,166 +82,6 @@ const show_certificate_if_course_completed = (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
const quiz_summary = (e = undefined) => {
|
||||
e && e.preventDefault();
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let total_questions = $(".question").length;
|
||||
let self = this;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
|
||||
args: {
|
||||
quiz: quiz_name,
|
||||
results: localStorage.getItem(quiz_name),
|
||||
},
|
||||
callback: (data) => {
|
||||
$(".question").addClass("hide");
|
||||
$("#summary").addClass("hide");
|
||||
$("#quiz-form")
|
||||
.parent()
|
||||
.prepend(
|
||||
`<div class="summary">
|
||||
<div class="font-weight-bold"> ${__("Score")}: ${
|
||||
data.message
|
||||
}/${total_questions} </div>
|
||||
</div>`
|
||||
);
|
||||
$("#try-again").removeClass("hide");
|
||||
self.quiz_submitted = true;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const try_quiz_again = (e) => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const check_answer = (e = undefined) => {
|
||||
e && e.preventDefault();
|
||||
|
||||
let answer = $(".active-question textarea");
|
||||
if (answer.length && !answer.val().trim()) {
|
||||
frappe.throw(__("Please enter your answer"));
|
||||
}
|
||||
|
||||
clearInterval(self.timer);
|
||||
$(".timer").addClass("hide");
|
||||
|
||||
let total_questions = $(".question").length;
|
||||
let current_index = $(".active-question").attr("data-qt-index");
|
||||
|
||||
$(".explanation").removeClass("hide");
|
||||
$("#check").addClass("hide");
|
||||
|
||||
if (current_index == total_questions) {
|
||||
if ($(".eligible-for-submission").length) {
|
||||
$("#summary").removeClass("hide");
|
||||
} else {
|
||||
$("#submission-message").removeClass("hide");
|
||||
}
|
||||
} else {
|
||||
$("#next").removeClass("hide");
|
||||
}
|
||||
parse_options();
|
||||
};
|
||||
|
||||
const parse_options = () => {
|
||||
let type = $(".active-question").data("type");
|
||||
|
||||
if (type == "Choices") {
|
||||
$(".active-question input").each((i, element) => {
|
||||
is_answer_correct(type, element);
|
||||
});
|
||||
} else {
|
||||
is_answer_correct(type, $(".active-question textarea"));
|
||||
}
|
||||
};
|
||||
|
||||
const is_answer_correct = (type, element) => {
|
||||
let answer = decodeURIComponent($(element).val());
|
||||
|
||||
frappe.call({
|
||||
async: false,
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.check_answer",
|
||||
args: {
|
||||
question: $(".active-question").data("name"),
|
||||
type: type,
|
||||
answer: answer,
|
||||
},
|
||||
callback: (data) => {
|
||||
type == "Choices"
|
||||
? parse_choices(element, data.message)
|
||||
: parse_possible_answers(element, data.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const parse_choices = (element, correct) => {
|
||||
if ($(element).prop("checked")) {
|
||||
self.answer.push(decodeURIComponent($(element).val()));
|
||||
correct && self.is_correct.push(1);
|
||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
||||
} else {
|
||||
correct && self.is_correct.push(0);
|
||||
correct
|
||||
? add_icon(element, "minus-circle-green")
|
||||
: add_icon(element, "minus-circle");
|
||||
}
|
||||
};
|
||||
|
||||
const parse_possible_answers = (element, correct) => {
|
||||
self.answer.push(decodeURIComponent($(element).val()));
|
||||
if (correct) {
|
||||
self.is_correct.push(1);
|
||||
show_indicator("success", element);
|
||||
} else {
|
||||
self.is_correct.push(0);
|
||||
show_indicator("failure", element);
|
||||
}
|
||||
};
|
||||
|
||||
const show_indicator = (class_name, element) => {
|
||||
let label = class_name == "success" ? "Correct" : "Incorrect";
|
||||
let icon =
|
||||
class_name == "success" ? "#icon-solid-success" : "#icon-solid-error";
|
||||
$(`<div class="answer-indicator ${class_name}">
|
||||
<svg class="icon icon-md">
|
||||
<use href=${icon}>
|
||||
</svg>
|
||||
<span style="font-weight: 500">${__(label)}</span>
|
||||
</div>`).insertAfter(element);
|
||||
};
|
||||
|
||||
const add_icon = (element, icon) => {
|
||||
$(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>
|
||||
<img class="d-inline mr-3" src="/assets/lms/icons/${icon}.svg">
|
||||
${label}
|
||||
</div>
|
||||
`);
|
||||
};
|
||||
|
||||
const add_to_local_storage = () => {
|
||||
let current_index = $(".active-question").attr("data-qt-index");
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
|
||||
let quiz_obj = {
|
||||
question_index: current_index,
|
||||
answer: self.answer.join(),
|
||||
is_correct: self.is_correct,
|
||||
};
|
||||
|
||||
quiz_stored ? quiz_stored.push(quiz_obj) : (quiz_stored = [quiz_obj]);
|
||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored));
|
||||
|
||||
self.answer = [];
|
||||
self.is_correct = [];
|
||||
};
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(".title").attr("data-course");
|
||||
@@ -483,35 +250,3 @@ const fetch_assignments = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const initialize_timer = () => {
|
||||
this.time_left = $(".timer").data("time");
|
||||
calculate_and_display_time(100, this.time_left);
|
||||
$(".timer").removeClass("hide");
|
||||
const total_time = $(".timer").data("time");
|
||||
this.start_time = new Date().getTime();
|
||||
const self = this;
|
||||
let old_diff;
|
||||
|
||||
this.timer = setInterval(function () {
|
||||
var diff = (new Date().getTime() - self.start_time) / 1000;
|
||||
var variation = old_diff ? diff - old_diff : diff;
|
||||
old_diff = diff;
|
||||
self.time_left -= variation;
|
||||
let percent_time = (self.time_left / total_time) * 100;
|
||||
calculate_and_display_time(percent_time);
|
||||
if (self.time_left <= 0) {
|
||||
clearInterval(self.timer);
|
||||
$(".timer").addClass("hide");
|
||||
check_answer();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const calculate_and_display_time = (percent_time) => {
|
||||
$(".timer .progress-bar").attr("aria-valuenow", percent_time);
|
||||
$(".timer .progress-bar").attr("aria-valuemax", percent_time);
|
||||
$(".timer .progress-bar").css("width", `${percent_time}%`);
|
||||
let progress_color = percent_time < 20 ? "red" : "var(--primary-color)";
|
||||
$(".timer .progress-bar").css("background-color", progress_color);
|
||||
};
|
||||
|
||||
@@ -260,49 +260,58 @@
|
||||
{{ _("Create New") }}
|
||||
</div>
|
||||
<p class="field-description">
|
||||
{{ _("To create a new assignment for this class, click on the create assignment button. Once you have created the new assignment you can come back to the class and add the assignment from here.") }}
|
||||
{{ _("To create a new assignment or quiz for this class, click on the buttons below. Once you have created the new assignment or quiz you can come back and add it from here.") }}
|
||||
</p>
|
||||
<div>
|
||||
<a class="btn btn-default btn-sm" href="/assignments/new-assignment">
|
||||
<div class="flex">
|
||||
<a class="btn btn-secondary btn-sm" href="/assignments/new-assignment">
|
||||
{{ _("Create Assignment") }}
|
||||
</a>
|
||||
<a class="btn btn-secondary btn-sm ml-2" href="/assignments/new-quiz">
|
||||
{{ _("Create Quiz") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="" id="assessment-form">
|
||||
{% if all_assignments | length %}
|
||||
<div>
|
||||
<div class="field-label mb-2">
|
||||
{{ _("Assignments") }}
|
||||
</div>
|
||||
<p class="field-description">
|
||||
{{ _("Select the assignments you wish to include for the assessment of this class. Your selections will be automatically saved upon clicking. If you decide to remove an item from the list, simply uncheck it.") }}
|
||||
</p>
|
||||
{% for assignment in all_assignments %}
|
||||
<div>
|
||||
<label class="vertically-center">
|
||||
<input type="checkbox" class="assessment-item" {% if assignment.checked %} checked {% endif %} value="{{ assignment.name }}" data-type="LMS Assignment" data-name="{{ assignment.name }}">
|
||||
{{ assignment.title }}
|
||||
</label>
|
||||
<div class="field-label mb-2">
|
||||
{{ _("Select Assessments") }}
|
||||
</div>
|
||||
<p class="field-description">
|
||||
{{ _("Select the assessments you wish to include for this class. Your selections will be automatically saved upon clicking. If you decide to remove an item from the list, simply uncheck it.") }}
|
||||
</p>
|
||||
<div class="flex justify-content-between">
|
||||
{% if all_assignments | length %}
|
||||
<div>
|
||||
<div class="field-label mb-2">
|
||||
{{ _("Assignments") }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- {% if all_quizzes | length %}
|
||||
<div>
|
||||
<div class="field-label mb-2">
|
||||
{{ _("Quiz") }}
|
||||
{% for assignment in all_assignments %}
|
||||
<div>
|
||||
<label class="vertically-center">
|
||||
<input type="checkbox" class="assessment-item" {% if assignment.checked %} checked {% endif %} value="{{ assignment.name }}" data-type="LMS Assignment" data-name="{{ assignment.name }}">
|
||||
{{ assignment.title }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for quiz in all_quizzes %}
|
||||
<div>
|
||||
<label class="vertically-center">
|
||||
<input type="checkbox" class="assessment-item" {% if quiz.checked %} checked {% endif %} value="{{ quiz.name }}" data-type="LMS Quiz" data-name="{{ quiz.name }}">
|
||||
{{ quiz.title }}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if all_quizzes | length %}
|
||||
<div>
|
||||
<div class="field-label mb-2">
|
||||
{{ _("Quizzes") }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %} -->
|
||||
{% for quiz in all_quizzes %}
|
||||
<div>
|
||||
<label class="vertically-center">
|
||||
<input type="checkbox" class="assessment-item" {% if quiz.checked %} checked {% endif %} value="{{ quiz.name }}" data-type="LMS Quiz" data-name="{{ quiz.name }}">
|
||||
{{ quiz.title }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -76,9 +76,8 @@ def get_context(context):
|
||||
|
||||
|
||||
def get_all_quizzes(class_name):
|
||||
all_quizzes = frappe.get_all(
|
||||
"LMS Quiz", {"owner": frappe.session.user}, ["name", "title"]
|
||||
)
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
all_quizzes = frappe.get_all("LMS Quiz", filters, ["name", "title"])
|
||||
for quiz in all_quizzes:
|
||||
quiz.checked = frappe.db.exists(
|
||||
{
|
||||
@@ -92,9 +91,8 @@ def get_all_quizzes(class_name):
|
||||
|
||||
|
||||
def get_all_assignments(class_name):
|
||||
all_assignments = frappe.get_all(
|
||||
"LMS Assignment", {"owner": frappe.session.user}, ["name", "title"]
|
||||
)
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
all_assignments = frappe.get_all("LMS Assignment", filters, ["name", "title"])
|
||||
for assignment in all_assignments:
|
||||
assignment.checked = frappe.db.exists(
|
||||
{
|
||||
|
||||
58
lms/www/quiz_submission/quiz_submission.html
Normal file
58
lms/www/quiz_submission/quiz_submission.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ quiz.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width">
|
||||
{{ SubmissionForm(quiz) }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky mb-5">
|
||||
<div class="container form-width">
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="vertically-center">
|
||||
<div class="page-title">
|
||||
{{ quiz.title }}
|
||||
</div>
|
||||
{% if submission.score %}
|
||||
<div>
|
||||
{{ submission.score }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/classes">
|
||||
{{ _("All Classes") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="align-self-center">
|
||||
<!-- <button class="btn btn-primary btn-sm btn-save-assignment" {% if quiz.name %} data-quiz="{{ quiz.name }}" {% endif %}
|
||||
{% if submission.name %} data-submission="{{ submission.name }}" {% endif %}>
|
||||
{{ _("Save") }}
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro SubmissionForm(quiz) %}
|
||||
{% include("lms/templates/quiz/quiz.html") %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
{% include "lms/templates/quiz/quiz.js" %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
31
lms/www/quiz_submission/quiz_submission.py
Normal file
31
lms/www/quiz_submission/quiz_submission.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import frappe
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
submission = frappe.form_dict["submission"]
|
||||
quiz_name = frappe.form_dict["quiz"]
|
||||
|
||||
context.quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
||||
|
||||
if submission == "new-submission":
|
||||
context.submission = frappe._dict()
|
||||
else:
|
||||
context.submission = frappe.db.get_value(
|
||||
"LMS Quiz Submission",
|
||||
submission,
|
||||
["name", "score", "member", "member_name"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not context.is_moderator and frappe.session.user != context.submission.member:
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
if not context.assignment or not context.submission:
|
||||
raise frappe.PermissionError(_("Invalid Submission URL"))
|
||||
@@ -71,35 +71,61 @@ def get_assessments(class_name, member=None):
|
||||
|
||||
for assessment in assessments:
|
||||
if assessment.assessment_type == "LMS Assignment":
|
||||
assessment.title = frappe.db.get_value(
|
||||
"LMS Assignment", assessment.assessment_name, "title"
|
||||
)
|
||||
|
||||
existing_submission = frappe.db.exists(
|
||||
{
|
||||
"doctype": "LMS Assignment Submission",
|
||||
"member": member,
|
||||
"assignment": assessment.assessment_name,
|
||||
}
|
||||
)
|
||||
|
||||
if existing_submission:
|
||||
assessment.submission = frappe.db.get_value(
|
||||
"LMS Assignment Submission",
|
||||
existing_submission,
|
||||
["name", "status", "comments"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
assessment.edit_url = f"/assignments/{assessment.assessment_name}"
|
||||
submission_name = existing_submission if existing_submission else "new-submission"
|
||||
assessment.url = (
|
||||
f"/assignment-submission/{assessment.assessment_name}/{submission_name}"
|
||||
)
|
||||
assessment = get_assignment_details(assessment, member)
|
||||
|
||||
elif assessment.assessment_type == "LMS Quiz":
|
||||
assessment.title = frappe.db.get_value(
|
||||
"LMS Quiz", assessment.assessment_name, "title"
|
||||
)
|
||||
assessment.url = f"/quizzes/{assessment.assessment_name}"
|
||||
assessment = get_quiz_details(assessment, member)
|
||||
|
||||
return assessments
|
||||
|
||||
|
||||
def get_assignment_details(assessment, member):
|
||||
assessment.title = frappe.db.get_value(
|
||||
"LMS Assignment", assessment.assessment_name, "title"
|
||||
)
|
||||
|
||||
existing_submission = frappe.db.exists(
|
||||
{
|
||||
"doctype": "LMS Assignment Submission",
|
||||
"member": member,
|
||||
"assignment": assessment.assessment_name,
|
||||
}
|
||||
)
|
||||
|
||||
if existing_submission:
|
||||
assessment.submission = frappe.db.get_value(
|
||||
"LMS Assignment Submission",
|
||||
existing_submission,
|
||||
["name", "status", "comments"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
assessment.edit_url = f"/assignments/{assessment.assessment_name}"
|
||||
submission_name = existing_submission if existing_submission else "new-submission"
|
||||
assessment.url = (
|
||||
f"/assignment-submission/{assessment.assessment_name}/{submission_name}"
|
||||
)
|
||||
|
||||
|
||||
def get_quiz_details(assessment, member):
|
||||
assessment.title = frappe.db.get_value("LMS Quiz", assessment.assessment_name, "title")
|
||||
|
||||
existing_submission = frappe.db.exists(
|
||||
{
|
||||
"doctype": "LMS Quiz Submission",
|
||||
"member": member,
|
||||
"quiz": assessment.assessment_name,
|
||||
}
|
||||
)
|
||||
|
||||
if existing_submission:
|
||||
assessment.submission = frappe.db.get_value(
|
||||
"LMS Quiz Submission",
|
||||
existing_submission,
|
||||
["name", "score"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
assessment.edit_url = f"/quizzes/{assessment.assessment_name}"
|
||||
submission_name = existing_submission if existing_submission else "new-submission"
|
||||
assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}"
|
||||
|
||||
Reference in New Issue
Block a user