Compare commits

..
13 Commits
10 changed files with 40 additions and 38 deletions
+4 -2
View File
@@ -783,10 +783,12 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
if self.start_date >= timezone.now() or ( if self.start_date >= timezone.now() or (
self.end_date is not None and self.end_date <= timezone.now() self.end_date is not None and self.end_date <= timezone.now()
): ):
raise Http404("Exam not found") # Raise with an explanatory message so the frontend can
# surface the reason to the end user.
raise Http404("Exam not currently available (outside allowed dates)")
# If it is not active no one can take # If it is not active no one can take
elif active_only and not self.active: elif active_only and not self.active:
raise Http404("Exam not found") raise Http404("Exam currently inactive")
# If candidates only check they have access # If candidates only check they have access
if self.candidates_only: if self.candidates_only:
+1 -1
View File
@@ -455,7 +455,7 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
name="exam_user_bulk_edit", name="exam_user_bulk_edit",
), ),
path( path(
"exam/<int:pk>/scores", "exam/<int:pk>/scores/all",
generic_exam_view.exam_scores_all, generic_exam_view.exam_scores_all,
name="exam_scores_all", name="exam_scores_all",
), ),
+2 -1
View File
@@ -3576,7 +3576,7 @@ class ExamViews(View, LoginRequiredMixin):
# Load questions; prefetch 'answers' for apps that use them to avoid repeated DB hits # Load questions; prefetch 'answers' for apps that use them to avoid repeated DB hits
questions_qs = exam.get_questions() questions_qs = exam.get_questions()
if self.app_name not in ("physics",): if self.app_name not in ("physics", "sbas"):
questions_qs = questions_qs.prefetch_related("answers") questions_qs = questions_qs.prefetch_related("answers")
# Materialize questions list for iteration # Materialize questions list for iteration
@@ -3694,6 +3694,7 @@ class ExamViews(View, LoginRequiredMixin):
#"cid_user_exam": cid_user_exam, #"cid_user_exam": cid_user_exam,
} }
template_context["normalised_score"] = None
if self.normalise_score is not None: if self.normalise_score is not None:
normalised_score = self.normalise_score(total_score) normalised_score = self.normalise_score(total_score)
template_context["normalised_score"] = normalised_score template_context["normalised_score"] = normalised_score
+4 -1
View File
@@ -686,7 +686,10 @@ def stats(request):
# HTTP Error 400 # HTTP Error 400
def page_not_found(request, exception): def page_not_found(request, exception):
response = render(request, "404.html") # Provide the exception message (reason) to the template so callers
# can show a user-friendly explanation when available.
context = {"reason": str(exception) if exception is not None else "Not found"}
response = render(request, "404.html", context=context)
response.status_code = 404 response.status_code = 404
+5 -2
View File
@@ -2,7 +2,7 @@
{% block content %} {% block content %}
<h2>Start exam: {{exam}}</h2> <h2>Start exam: {{ exam }}</h2>
{% if exam.time_limit %} {% if exam.time_limit %}
This exam has a time limit of {{ exam.get_time_limit }}. The time will start when you click the Start Exam button below.<br/> This exam has a time limit of {{ exam.get_time_limit }}. The time will start when you click the Start Exam button below.<br/>
@@ -44,7 +44,10 @@
let passcode = $("#passcode-box").val(); let passcode = $("#passcode-box").val();
if (Number.isInteger(parseInt(cid))) { if (Number.isInteger(parseInt(cid))) {
window.location.replace("{% url 'sbas:exam_take' pk=exam.pk sk=0 cid='0000000' passcode='ZZZZZZ' %}".replace("0000000", cid).replace("ZZZZZZ", passcode)); // reverse a URL with safe placeholders and replace them client-side
const template = "{% url 'sbas:exam_take' pk=exam.pk sk=0 cid=0 passcode='0' %}"; // e.g. /sbas/exam/1/0/0/0/take
const url = template.replace('/0/0/take', `/${cid}/${passcode}/take`);
window.location.replace(url);
} else { } else {
alert("Please enter a valid Candidate ID (CID).") alert("Please enter a valid Candidate ID (CID).")
} }
+5 -7
View File
@@ -1,15 +1,13 @@
<details class="help-text pt-3"> <details class="help-text pt-3">
<summary><i class="bi bi-info-circle"></i> Help</summary> <summary><i class="bi bi-info-circle"></i> Help</summary>
<p>Each question contains a list of 5 different statements. One of these is the single BEST answer.</p> <p>Each question contains a list of 5 different statements. One of these is the single BEST answer.</p>
<p>Click on the correct statement to select it. Once selected it will be highlighted. </p> <p>Click on the correct statement to select it. Once selected it will be highlighted. </p>
<p>Your answers are saved when navigating between questions (or if the save button is clicked on the final question). An overview of all the questions can be seen by clicking on the overview button.</p> <p>Your answers are saved when navigating between questions (or if the save button is clicked on the final question). An overview of all the questions can be seen by clicking on the overview button.</p>
<p>Results are also available on the results page
<a target="_blank" href="
{% if cid %} {% if cid %}
{% url 'sbas:exam_scores_cid_user' pk=exam.pk cid=cid passcode=passcode %} {% url 'sbas:exam_scores_cid_user' pk=exam.pk cid=cid passcode=passcode as scores_url %}
{% else %} {% else %}
{% url 'sbas:exam_scores_user' pk=exam.pk %} {% url 'sbas:exam_scores_user' pk=exam.pk as scores_url %}
{% endif %} {% endif %}
">here</a>.</p> <p>Results are also available on the results page <a target="_blank" href="{{ scores_url }}">here</a>.</p>
</details> </details>
-10
View File
@@ -134,16 +134,6 @@ urlpatterns = [
), ),
path("question/<int:pk>/toggle_frcr/", views.toggle_frcr_appropriate, name="toggle_frcr"), path("question/<int:pk>/toggle_frcr/", views.toggle_frcr_appropriate, name="toggle_frcr"),
path("category/merge/", views.merge_category, name="merge_category"), path("category/merge/", views.merge_category, name="merge_category"),
#path(
# "exam/<int:pk>/scores/<int:cid>/<str:passcode>/",
# views.exam_scores_cid_user,
# name="exam_scores_cid_user",
#),
#path(
# "exam/<int:pk>/scores/",
# views.exam_scores_cid_user,
# name="exam_scores_user",
#),
] ]
urlpatterns.extend(generic_view_urls(views.GenericViews)) urlpatterns.extend(generic_view_urls(views.GenericViews))
+1
View File
@@ -370,6 +370,7 @@ def exam_take_overview(request, pk, cid=None, passcode=None):
def exam_take(request, pk: int, sk: int, cid: int = None, passcode: str = None): def exam_take(request, pk: int, sk: int, cid: int = None, passcode: str = None):
logger.debug(f"Taking exam pk={pk} sk={sk} cid={cid} passcode={passcode} user={request.user}")
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
if not exam.active: if not exam.active:
+9 -5
View File
@@ -1,10 +1,14 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class=""> <div class="">
<h2>404 error</h2> <h2>404 error</h2>
{{ reason }} {% if reason %}
{{ reason }}
{% endif %}
<h3>{{resolved}}</h3> {% if resolved %}
</div> <h3>{{ resolved }}</h3>
{% endif %}
</div>
{% endblock %} {% endblock %}
+2 -2
View File
@@ -32,7 +32,7 @@
<h2>Exam: {{ exam }}</h2> <h2>Exam: {{ exam }}</h2>
<h3>Candidate: {{ cid|default_if_none:request.user.username }}</h3> <h3>Candidate: {{ cid|default_if_none:request.user.username }}</h3>
{% if cid_user_exam %} {% comment %} {% if cid_user_exam %}
{% if exam.results_supervisor_visible %} {% if exam.results_supervisor_visible %}
Exam results are visible to supervisor.<br/> Exam results are visible to supervisor.<br/>
{% else %} {% else %}
@@ -42,5 +42,5 @@
>Toggle</button> >Toggle</button>
<br/> <br/>
{% endif %} {% endif %}
{% endif %} {% endif %} {% endcomment %}
Answers: Answers: