fix: quiz max attempts

This commit is contained in:
Jannat Patel
2023-06-21 20:11:30 +05:30
parent da72513f6a
commit 7d18e1d928
14 changed files with 262 additions and 162 deletions

View File

@@ -39,7 +39,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "0", "default": "1",
"fieldname": "max_attempts", "fieldname": "max_attempts",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Max Attempts" "label": "Max Attempts"
@@ -48,6 +48,7 @@
"default": "0", "default": "0",
"fieldname": "time", "fieldname": "time",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 1,
"label": "Time Per Question (in Seconds)" "label": "Time Per Question (in Seconds)"
}, },
{ {
@@ -69,7 +70,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-11-15 15:36:39.585488", "modified": "2023-06-21 09:13:01.322701",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",

View File

@@ -2,12 +2,11 @@
# For license information, please see license.txt # For license information, please see license.txt
import json import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr from frappe.utils import cstr
from lms.lms.utils import generate_slug, has_course_moderator_role from lms.lms.utils import generate_slug, has_course_moderator_role, can_create_courses
class LMSQuiz(Document): class LMSQuiz(Document):
@@ -126,7 +125,7 @@ def quiz_summary(quiz, results):
score += correct score += correct
del result["question_index"] del result["question_index"]
frappe.get_doc( submission = frappe.get_doc(
{ {
"doctype": "LMS Quiz Submission", "doctype": "LMS Quiz Submission",
"quiz": quiz, "quiz": quiz,
@@ -134,19 +133,32 @@ def quiz_summary(quiz, results):
"score": score, "score": score,
"member": frappe.session.user, "member": frappe.session.user,
} }
).save(ignore_permissions=True) )
submission.save(ignore_permissions=True)
return score return {
"score": score,
"submission": submission.name,
}
@frappe.whitelist() @frappe.whitelist()
def save_quiz(quiz_title, quiz): def save_quiz(quiz_title, max_attempts=1, quiz=None):
if not can_create_courses():
return
values = {
"title": quiz_title,
"max_attempts": max_attempts,
}
if quiz: if quiz:
frappe.db.set_value("LMS Quiz", quiz, "title", quiz_title) print(max_attempts)
frappe.db.set_value("LMS Quiz", quiz, values)
return quiz return quiz
else: else:
doc = frappe.new_doc("LMS Quiz") doc = frappe.new_doc("LMS Quiz")
doc.update({"title": quiz_title}) doc.update(values)
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
return doc.name return doc.name

View File

@@ -110,19 +110,29 @@ def quiz_renderer(quiz_name):
+"</div>" +"</div>"
quiz = frappe.get_doc("LMS Quiz", quiz_name) quiz = frappe.get_doc("LMS Quiz", quiz_name)
context = {"quiz": quiz}
no_of_attempts = frappe.db.count( no_of_attempts = frappe.db.count(
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name} "LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
) )
if quiz.max_attempts and no_of_attempts >= quiz.max_attempts: all_submissions = frappe.get_all(
last_attempt_score = frappe.db.get_value( "LMS Quiz Submission",
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}, ["score"] {
) "quiz": quiz.name,
"member": frappe.session.user,
},
["name", "score", "creation"],
order_by="creation desc",
)
context.update({"attempts_exceeded": True, "last_attempt_score": last_attempt_score}) return frappe.render_template(
return frappe.render_template("templates/quiz/quiz.html", context) "templates/quiz/quiz.html",
{
"quiz": quiz,
"no_of_attempts": no_of_attempts,
"all_submissions": all_submissions,
"no_of_attempts": no_of_attempts,
},
)
def exercise_renderer(argument): def exercise_renderer(argument):

View File

@@ -1,16 +1,26 @@
{% if attempts_exceeded %} {% if not hide_quiz %}
<div class=""> <div class="mt-4">
<h2 class="mt-3"> <ul class="alert alert-info pl-8">
{{ quiz.title }}
</h2> <li>
<div class="alert alert-info medium mb-0"> {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
{{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }} </li>
{{ _("Your latest score is {0}.").format(last_attempt_score) }}
</div> {% if quiz.max_attempts %}
</div> {% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
<li>
{{ _("You can attempt this quiz only {0} {1}").format(quiz.max_attempts, suffix) }}
</li>
{% endif %}
{% if quiz.time %}
<li class="alert alert-info medium">
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
</li>
{% endif %}
</ul>
{% else %}
<div class="">
<div id="start-banner" class="common-card-style column-card align-items-center"> <div id="start-banner" class="common-card-style column-card align-items-center">
<div class="text-center my-10"> <div class="text-center my-10">
@@ -18,30 +28,17 @@
{{ quiz.title }} {{ quiz.title }}
</div> </div>
<div class=""> {% if not quiz.max_attempts or no_of_attempts < quiz.max_attempts %}
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} <button class="btn btn-secondary btn-sm btn-start-quiz mt-4">
{{ _("Start") }}
</button>
{% else %}
<div>
{{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }}
</div>
{% endif %}
</div> </div>
{% if quiz.max_attempts %}
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
<div class="alert alert-info medium">
{{ _("This quiz can only be taken {0} {1}. If you attempt the quiz but leave the page before submitting,
the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }}
</div>
{% endif %}
{% if quiz.time %}
<div class="alert alert-info medium">
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
</div>
{% endif %}
<button class="btn btn-secondary btn-sm btn-start-quiz mt-4">
{{ _("Start the Quiz") }}
</button>
</div>
</div> </div>
<form id="quiz-form" class="common-card-style column-card hide"> <form id="quiz-form" class="common-card-style column-card hide">
@@ -108,20 +105,50 @@
</div> </div>
{% endif %} {% endif %}
<button class="btn btn-secondary btn-sm pull-right" id="check" disabled> <button class="btn btn-secondary btn-sm pull-right" id="check" disabled>
{{ _("Check") }} {{ _("Check") }}
</button> </button>
<div class="btn btn-secondary btn-sm hide" id="next"> <button class="btn btn-secondary btn-sm hide" id="next">
{{ _("Next Question") }} {{ _("Next Question") }}
</div> </button>
<div class="btn btn-secondary btn-sm hide" id="summary"> <button class="btn btn-secondary btn-sm hide" id="summary">
{{ _("Submit") }} {{ _("Submit") }}
</div> </button>
<div class="btn btn-secondary btn-sm hide" id="try-again"> <button class="btn btn-secondary btn-sm mx-auto hide" id="try-again">
{{ _("Try Again") }} {{ _("Try Again") }}
</div> </button>
</div> </div>
</form> </form>
</div> </div>
{% endif %} {% endif %}
{% if all_submissions | length %}
<article class="mt-8">
<div class="field-label">
{{ _("All Submissions") }}
</div>
<div class="form-grid">
<div class="grid-heading-row">
<div class="grid-row">
<div class="data-row row">
<div class="col grid-static-col">{{ _("No.") }}</div>
<div class="col grid-static-col">{{ _("Date") }}</div>
<div class="col grid-static-col text-right">{{ _("Score") }}</div>
</div>
</div>
</div>
<div>
{% for submission in all_submissions %}
<div class="grid-row">
<div class="data-row row">
<div class="col grid-static-col">{{ loop.index }}</div>
<div class="col grid-static-col">{{ frappe.utils.format_datetime(submission.creation, "medium") }}</div>
<div class="col grid-static-col text-right">{{ submission.score }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
</article>
{% endif %}

View File

@@ -3,6 +3,7 @@ frappe.ready(() => {
this.answer = []; this.answer = [];
this.is_correct = []; this.is_correct = [];
const self = this; const self = this;
localStorage.removeItem($("#quiz-title").data("name"));
$(".btn-start-quiz").click((e) => { $(".btn-start-quiz").click((e) => {
$("#start-banner").addClass("hide"); $("#start-banner").addClass("hide");
@@ -19,15 +20,18 @@ frappe.ready(() => {
}); });
$("#summary").click((e) => { $("#summary").click((e) => {
e.preventDefault();
add_to_local_storage(); add_to_local_storage();
quiz_summary(e); quiz_summary(e);
}); });
$("#check").click((e) => { $("#check").click((e) => {
e.preventDefault();
check_answer(e); check_answer(e);
}); });
$("#next").click((e) => { $("#next").click((e) => {
e.preventDefault();
add_to_local_storage(); add_to_local_storage();
mark_active_question(e); mark_active_question(e);
}); });
@@ -35,15 +39,6 @@ frappe.ready(() => {
$("#try-again").click((e) => { $("#try-again").click((e) => {
try_quiz_again(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) => { const mark_active_question = (e = undefined) => {
@@ -122,13 +117,15 @@ const quiz_summary = (e = undefined) => {
callback: (data) => { callback: (data) => {
$(".question").addClass("hide"); $(".question").addClass("hide");
$("#summary").addClass("hide"); $("#summary").addClass("hide");
$(".quiz-footer").prepend( $(".quiz-footer span").addClass("hide");
`<div class="summary"> $("#quiz-form").prepend(
<div class="font-weight-bold"> ${__("Score")}: ${ `<div class="summary bold-heading text-center">
data.message ${__("Your score is ")} ${data.message.score} ${__(
}/${total_questions} </div> " out of "
</div>` )} ${total_questions}
</div>`
); );
$("#try-again").data("submission", data.message.submission);
$("#try-again").removeClass("hide"); $("#try-again").removeClass("hide");
self.quiz_submitted = true; self.quiz_submitted = true;
}, },
@@ -136,7 +133,14 @@ const quiz_summary = (e = undefined) => {
}; };
const try_quiz_again = (e) => { const try_quiz_again = (e) => {
window.location.reload(); if (window.location.href.includes("new-submission")) {
window.location.href = window.location.pathname.replace(
"new-submission",
$
);
} else {
window.location.reload();
}
}; };
const check_answer = (e = undefined) => { const check_answer = (e = undefined) => {

View File

@@ -32,6 +32,8 @@
<a class="dark-links" href="/classes"> <a class="dark-links" href="/classes">
{{ _("All Classes") }} {{ _("All Classes") }}
</a> </a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ _("Assignment Submission") }}</span>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,6 @@ frappe.ready(() => {
let self = this; let self = this;
frappe.telemetry.capture("on_lesson_page", "lms"); frappe.telemetry.capture("on_lesson_page", "lms");
localStorage.removeItem($("#quiz-title").data("name"));
fetch_assignments(); fetch_assignments();

View File

@@ -58,13 +58,16 @@
</div> </div>
</div> </div>
{% if quiz.name %}
<div class="align-self-center"> <div class="align-self-center">
<button class="btn btn-primary btn-sm btn-add-question"> {% if quiz.name %}
<button class="btn btn-secondary btn-sm btn-add-question mr-2">
{{ _("Add Question") }} {{ _("Add Question") }}
</button> </button>
{% endif %}
<button class="btn btn-primary btn-sm btn-save-quiz">
{{ _("Save") }}
</button>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
@@ -86,6 +89,19 @@
<input type="text" class="field-input" id="quiz-title" {% if quiz.name %} value="{{ quiz.title }}" data-title="{{ quiz.title }}" {% endif %}> <input type="text" class="field-input" id="quiz-title" {% if quiz.name %} value="{{ quiz.title }}" data-title="{{ quiz.title }}" {% endif %}>
</div> </div>
</div> </div>
<div class="field-group">
<div class="field-label">
{{ _("Max Attempts") }}
</div>
<div class="field-description">
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
</div>
<div>
{% set max_attempts = quiz.max_attempts if quiz.name else 1 %}
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
</div>
</div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -1,8 +1,9 @@
frappe.ready(() => { frappe.ready(() => {
$("#quiz-title").focusout((e) => { $(".btn-save-quiz").click((e) => {
if ($("#quiz-title").val() != $("#quiz-title").data("title")) { save_quiz({
save_quiz({ quiz_title: $("#quiz-title").val() }); quiz_title: $("#quiz-title").val(),
} max_attempts: $("#max-attempts").val(),
});
}); });
$(".question-row").click((e) => { $(".question-row").click((e) => {
@@ -14,26 +15,6 @@ frappe.ready(() => {
}); });
}); });
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 show_question_modal = (values = {}) => { const show_question_modal = (values = {}) => {
let fields = get_question_fields(values); let fields = get_question_fields(values);
@@ -142,6 +123,7 @@ const save_quiz = (values) => {
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz", method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: { args: {
quiz_title: values.quiz_title, quiz_title: values.quiz_title,
max_attempts: values.max_attempts,
quiz: $("#quiz-form").data("name") || "", quiz: $("#quiz-form").data("name") || "",
}, },
callback: (data) => { callback: (data) => {

View File

@@ -20,7 +20,9 @@ def get_context(context):
else: else:
fields_arr = ["name", "question", "type"] fields_arr = ["name", "question", "type"]
context.quiz = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1) context.quiz = frappe.db.get_value(
"LMS Quiz", quizname, ["title", "name", "max_attempts"], as_dict=1
)
context.quiz.questions = frappe.get_all( context.quiz.questions = frappe.get_all(
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx" "LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
) )

View File

@@ -1,6 +1,6 @@
{% extends "lms/templates/lms_base.html" %} {% extends "lms/templates/lms_base.html" %}
{% block title %} {% block title %}
{{ student.first_name }} 's {{ _("Progress") }} {{ student.first_name }}'s {{ _("Progress") }}
{% endblock %} {% endblock %}
@@ -15,61 +15,93 @@
{% macro Header() %} {% macro Header() %}
<header class="sticky mb-5"> <header class="sticky mb-5">
<div class="container form-width"> <div class="container form-width">
<div class="edit-header"> <div class="edit-header">
<div> <div>
<div class="page-title"> <div class="page-title">
{{ _("{0}'s Progress").format(student.full_name) }} {{ _("{0}").format(student.full_name) }}
</div> </div>
<div class="vertically-center small"> <div class="vertically-center small">
<a class="dark-links" href="/classes"> <a class="dark-links" href="/classes">
{{ _("All Classes") }} {{ _("All Classes") }}
</a> </a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg"> <img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<a class="dark-links" href="/classes/{{ class_info.name }}"> <a class="dark-links" href="/classes/{{ class_info.name }}">
{{ class_info.name }} {{ class_info.name }}
</a> </a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg"> <img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ _("{0}'s Progress").format(student.full_name) }}</span> <span class="breadcrumb-destination">{{ _("Student Progress").format(student.full_name) }}</span>
</div> </div>
</div> </div>
{% if is_moderator %} {% if is_moderator %}
<div class="align-self-center"> <div class="align-self-center">
<a class="btn btn-primary btn-sm btn-evaluate" href=/evaluation/new?member={{student.name}}&date={{frappe.utils.getdate()}}&class={{class_info.name}}"> <a class="btn btn-default btn-sm mr-2" href="/users/{{ student.username }}">
{{ _("Evaluate") }} {{ _("View Profile") }}
</a> </a>
</div> <a class="btn btn-primary btn-sm btn-evaluate" href=/evaluation/new?member={{student.name}}&date={{frappe.utils.getdate()}}&class={{class_info.name}}">
{{ _("Evaluate") }}
</a>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</header> </header>
{% endmacro %} {% endmacro %}
{% macro Progress(class_info, student) %} {% macro Progress(class_info, student) %}
{% if assessments | length %} {% if assessments | length %}
<article> <article class="form-grid">
{% for assessment in assessments %} <div class="grid-heading-row">
<div class="list-row level"> <div class="grid-row">
<a {% if is_moderator and assessment.submission or frappe.session.user == student.name %} class="clickable" href="{{ assessment.url }}" {% endif %}> <div class="data-row row">
{{ assessment.title }} <div class="col grid-static-col">
</a> {{ _("Assessment") }}
</div>
{% if assessment.submission %} <div class="col grid-static-col">
{% set status = assessment.submission.status %} {{ _("Type") }}
{% set color = "green" if status == "Pass" else "red" if status == "Fail" else "orange" %} </div>
<div> <div class="col grid-static-col">
<div class="indicator-pill {{ color }}"> {{ _("Status/Score") }}
{{ assessment.submission.status }}
</div> </div>
</div> </div>
{% else %} </div>
<div class="indicator-pill red">
{{ _("Not Attempted") }} </div>
{% for assessment in assessments %}
{% set has_access = is_moderator and assessment.submission or frappe.session.user == student.name %}
<div class="grid-row">
<div class="data-row row">
<a class="col grid-static-col {% if has_access %} clickable {% endif %}" {% if has_access %} href="{{ assessment.url }}" {% endif %}>
{{ assessment.title }}
</a>
<div class="col grid-static-col">
{{ (assessment.assessment_type).split("LMS ")[1] }}
</div> </div>
{% endif %}
<div class="col grid-static-col mb-2">
{% if assessment.submission %}
{% if assessment.assessment_type == "LMS Assignment" %}
{% set status = assessment.submission.status %}
{% set color = "green" if status == "Pass" else "red" if status == "Fail" else "orange" %}
<div class="indicator-pill {{ color }}">
{{ status }}
</div>
{% else %}
<div>
{{ assessment.submission.score }}
</div>
{% endif %}
{% else %}
<div class="indicator-pill red">
{{ _("Not Attempted") }}
</div>
{% endif %}
</div>
</div>
</div> </div>
{% endfor %} {% endfor %}
</article> </article>

View File

@@ -21,16 +21,13 @@
<div class="page-title"> <div class="page-title">
{{ quiz.title }} {{ quiz.title }}
</div> </div>
{% if submission.score %}
<div>
{{ submission.score }}
</div>
{% endif %}
</div> </div>
<div class="vertically-center small"> <div class="vertically-center small">
<a class="dark-links" href="/classes"> <a class="dark-links" href="/classes">
{{ _("All Classes") }} {{ _("All Classes") }}
</a> </a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ _("Quiz Submission") }}</span>
</div> </div>
</div> </div>

View File

@@ -24,8 +24,25 @@ def get_context(context):
["name", "score", "member", "member_name"], ["name", "score", "member", "member_name"],
as_dict=True, as_dict=True,
) )
if not context.is_moderator and frappe.session.user != context.submission.member: if not context.is_moderator and frappe.session.user != context.submission.member:
raise frappe.PermissionError(_("You don't have permission to access this page.")) raise frappe.PermissionError(_("You don't have permission to access this page."))
if not context.assignment or not context.submission: if not context.quiz or not context.submission:
raise frappe.PermissionError(_("Invalid Submission URL")) raise frappe.PermissionError(_("Invalid Submission URL"))
context.all_submissions = frappe.get_all(
"LMS Quiz Submission",
{
"quiz": context.quiz.name,
"member": context.submission.member,
},
["name", "score", "creation"],
order_by="creation desc",
)
context.no_of_attempts = len(context.all_submissions) or 0
context.hide_quiz = (
context.is_moderator and context.submission.member != frappe.session.user
)
print(context.no_of_attempts)

View File

@@ -110,22 +110,21 @@ def get_assignment_details(assessment, member):
def get_quiz_details(assessment, member): def get_quiz_details(assessment, member):
assessment.title = frappe.db.get_value("LMS Quiz", assessment.assessment_name, "title") assessment.title = frappe.db.get_value("LMS Quiz", assessment.assessment_name, "title")
existing_submission = frappe.db.exists( existing_submission = frappe.get_all(
"LMS Quiz Submission",
{ {
"doctype": "LMS Quiz Submission",
"member": member, "member": member,
"quiz": assessment.assessment_name, "quiz": assessment.assessment_name,
} },
["name", "score"],
order_by="creation desc",
) )
if existing_submission: if len(existing_submission):
assessment.submission = frappe.db.get_value( assessment.submission = existing_submission[0]
"LMS Quiz Submission",
existing_submission,
["name", "score"],
as_dict=True,
)
assessment.edit_url = f"/quizzes/{assessment.assessment_name}" assessment.edit_url = f"/quizzes/{assessment.assessment_name}"
submission_name = existing_submission if existing_submission else "new-submission" submission_name = (
existing_submission[0].name if len(existing_submission) else "new-submission"
)
assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}" assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}"