feat: timer in quiz
This commit is contained in:
@@ -10,7 +10,9 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"questions",
|
"questions",
|
||||||
"lesson"
|
"lesson",
|
||||||
|
"max_attempts",
|
||||||
|
"time"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -31,11 +33,23 @@
|
|||||||
"label": "Lesson",
|
"label": "Lesson",
|
||||||
"options": "Course Lesson",
|
"options": "Course Lesson",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "max_attempts",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Max Attempts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "time",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Time Per Question (in Seconds)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-30 13:10:06.929358",
|
"modified": "2022-05-16 14:47:55.364743",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
@@ -57,5 +71,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,25 @@ def set_mandatory_fields_for_profile():
|
|||||||
|
|
||||||
def quiz_renderer(quiz_name):
|
def quiz_renderer(quiz_name):
|
||||||
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
||||||
context = dict(quiz=quiz)
|
|
||||||
|
context = {
|
||||||
|
"quiz": quiz
|
||||||
|
}
|
||||||
|
|
||||||
|
no_of_attempts = frappe.db.count("LMS Quiz Submission", {
|
||||||
|
"owner": frappe.session.user,
|
||||||
|
"quiz": quiz_name})
|
||||||
|
|
||||||
|
if quiz.max_attempts and no_of_attempts >= quiz.max_attempts:
|
||||||
|
last_attempt_score = frappe.db.get_value("LMS Quiz Submission", {
|
||||||
|
"owner": frappe.session.user,
|
||||||
|
"quiz": quiz_name
|
||||||
|
}, ["score"])
|
||||||
|
|
||||||
|
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.html", context)
|
||||||
|
|
||||||
def exercise_renderer(argument):
|
def exercise_renderer(argument):
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lesson-content-card {
|
.lesson-content-card {
|
||||||
margin-top: 1rem;
|
margin: 3rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-page {
|
.lesson-page {
|
||||||
@@ -1342,8 +1342,7 @@ pre {
|
|||||||
font-size: var(--text-lg);
|
font-size: var(--text-lg);
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-grow: 1;
|
width: 75%;
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-page-body {
|
.profile-page-body {
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
|
{% if attempts_exceeded %}
|
||||||
|
<div class="common-card-style text-center p-5" style="flex-direction: column;">
|
||||||
|
<div id="quiz-title" class="font-weight-bold mb-4" style="font-size: var(--text-lg);">{{ quiz.title }}</div>
|
||||||
|
<div> {{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }} </div>
|
||||||
|
<div> {{ _("You latest score is {0}.").format(last_attempt_score) }} </div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<div id="quiz-title" class="hide">{{ quiz.title }}</div>
|
<div id="quiz-title" class="hide">{{ quiz.title }}</div>
|
||||||
|
|
||||||
<div class="common-card-style question-card">
|
<div class="common-card-style question-card">
|
||||||
<form id="quiz-form">
|
<div id="start-banner" class="text-center">
|
||||||
|
<div class="font-weight-bold mb-4" style="font-size: var(--text-lg);"> {{ quiz.title }} </div>
|
||||||
|
<div> {{ _("This quiz has {0} questions.").format(quiz.questions | length) }} </div>
|
||||||
|
{% if quiz.time %}
|
||||||
|
<div> {{ _("This is a time bound quiz. You will have {0} seconds per question.").format(quiz.time) }} </div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="btn btn-primary btn-start-quiz"> {{ _("Start") }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="quiz-form" class="hide">
|
||||||
<div class="questions">
|
<div class="questions">
|
||||||
{% for question in quiz.questions %}
|
{% for question in quiz.questions %}
|
||||||
{% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %}
|
{% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %}
|
||||||
<div class="question {% if loop.index == 1 %} active-question {% else %} hide {% endif %}"
|
<div class="question hide"
|
||||||
data-question="{{ question.question }}" data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}">
|
data-question="{{ question.question }}" data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
||||||
<div class="question-header">
|
<div class="question-header">
|
||||||
<div class="question-number">{{ loop.index }}</div>
|
<div class="question-number">{{ loop.index }}</div>
|
||||||
<div class="question-text">
|
<div class="question-text">
|
||||||
<div class="course-meta pull-right ml-2"> {{ instruction }} </div>
|
{{ frappe.utils.md_to_html(question.question) }}
|
||||||
{{ frappe.utils.md_to_html(question.question) }}
|
</div>
|
||||||
</div>
|
<div class="course-meta"> {{ instruction }} </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
|
||||||
@@ -40,15 +56,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quiz-footer">
|
<div class="quiz-footer">
|
||||||
<span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span>
|
<span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span>
|
||||||
<button class="button pull-right is-default" id="check" disabled>{{ _("Check") }}</button>
|
|
||||||
<div class="button is-secondary hide" id="next">{{ _("Next Question") }}</div>
|
{% if quiz.time %}
|
||||||
<div class="button is-secondary is-default hide" id="summary">{{ _("Summary") }}</div>
|
<div class="progress timer w-75" data-time="{{ quiz.time }}">
|
||||||
<small id="submission-message" class="font-weight-bold hide"> {{ _("Please join the course to submit the Quiz.") }} </small>
|
<div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width:100%">
|
||||||
<div class="button is-secondary hide" id="try-again">{{ _("Try Again") }}</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button class="button pull-right is-default" id="check" disabled>{{ _("Check") }}</button>
|
||||||
|
<div class="button is-secondary hide" id="next">{{ _("Next Question") }}</div>
|
||||||
|
<div class="button is-secondary is-default hide" id="summary">{{ _("Summary") }}</div>
|
||||||
|
<small id="submission-message" class="font-weight-bold hide"> {{ _("Please join the course to submit the Quiz.") }} </small>
|
||||||
|
<div class="button is-secondary hide" id="try-again">{{ _("Try Again") }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="success-message"></h4>
|
<h4 class="success-message"></h4>
|
||||||
<h5 class="score text-muted"></h5>
|
<h5 class="score text-muted"></h5>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ frappe.ready(() => {
|
|||||||
clear_work(e);
|
clear_work(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".btn-start-quiz").click((e) => {
|
||||||
|
$("#start-banner").addClass("hide");
|
||||||
|
$("#quiz-form").removeClass("hide");
|
||||||
|
mark_active_question();
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const save_current_lesson = () => {
|
const save_current_lesson = () => {
|
||||||
@@ -65,19 +71,19 @@ const enable_check = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mark_active_question = (e = undefined) => {
|
const mark_active_question = (e = undefined) => {
|
||||||
var current_index;
|
$(".timer").addClass("hide");
|
||||||
var next_index = 1;
|
calculate_and_display_time(100);
|
||||||
if (e) {
|
$(".timer").removeClass("hide");
|
||||||
e.preventDefault();
|
|
||||||
current_index = $(".active-question").attr("data-qt-index");
|
let current_index = $(".active-question").attr("data-qt-index") || 0;
|
||||||
next_index = parseInt(current_index) + 1;
|
let next_index = parseInt(current_index) + 1;
|
||||||
}
|
$(".question").addClass("hide").removeClass("active-question");
|
||||||
$(".question").addClass("hide").removeClass("active-question");
|
$(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
|
||||||
$(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
|
$(".current-question").text(`${next_index}`);
|
||||||
$(".current-question").text(`${next_index}`);
|
$("#check").removeClass("hide").attr("disabled", true);
|
||||||
$("#check").removeClass("hide").attr("disabled", true);
|
$("#next").addClass("hide");
|
||||||
$("#next").addClass("hide");
|
$(".explanation").addClass("hide");
|
||||||
$(".explanation").addClass("hide");
|
initialize_timer();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mark_progress = (e) => {
|
const mark_progress = (e) => {
|
||||||
@@ -85,7 +91,7 @@ const mark_progress = (e) => {
|
|||||||
if ($(e.currentTarget).prop("nodeName") != "INPUT")
|
if ($(e.currentTarget).prop("nodeName") != "INPUT")
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
else
|
else
|
||||||
return
|
return;
|
||||||
|
|
||||||
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
|
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
|
||||||
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
|
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
|
||||||
@@ -179,30 +185,30 @@ const try_quiz_again = (e) => {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const check_answer = (e) => {
|
const check_answer = (e=undefined) => {
|
||||||
e.preventDefault();
|
e && e.preventDefault();
|
||||||
|
clearInterval(self.timer);
|
||||||
|
$(".timer").addClass("hide");
|
||||||
|
var quiz_name = $("#quiz-title").text();
|
||||||
|
var total_questions = $(".question").length;
|
||||||
|
var current_index = $(".active-question").attr("data-qt-index");
|
||||||
|
|
||||||
var quiz_name = $("#quiz-title").text();
|
$(".explanation").removeClass("hide");
|
||||||
var total_questions = $(".question").length;
|
$("#check").addClass("hide");
|
||||||
var current_index = $(".active-question").attr("data-qt-index");
|
|
||||||
|
|
||||||
$(".explanation").removeClass("hide");
|
if (current_index == total_questions) {
|
||||||
$("#check").addClass("hide");
|
if ($(".eligible-for-submission").length) {
|
||||||
|
$("#summary").removeClass("hide");
|
||||||
if (current_index == total_questions) {
|
}
|
||||||
if ($(".eligible-for-submission").length) {
|
else {
|
||||||
$("#summary").removeClass("hide")
|
$("#submission-message").removeClass("hide");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$("#submission-message").removeClass("hide");
|
$("#next").removeClass("hide");
|
||||||
}
|
}
|
||||||
}
|
var [answer, is_correct] = parse_options();
|
||||||
else {
|
add_to_local_storage(quiz_name, current_index, answer, is_correct);
|
||||||
$("#next").removeClass("hide")
|
|
||||||
}
|
|
||||||
|
|
||||||
var [answer, is_correct] = parse_options();
|
|
||||||
add_to_local_storage(quiz_name, current_index, answer, is_correct)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parse_options = () => {
|
const parse_options = () => {
|
||||||
@@ -381,3 +387,35 @@ 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);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user