+
Exam is in review mode. Add question feedback
here .
{% endif %}
-
{{question.stem|safe}}
+
{{question.stem|safe}}
@@ -50,35 +52,35 @@
{{ form.a.errors }}
- {{question.a|safe}}
+ {{question.a|safe}}
{{ form.a }}
{{ form.b.errors }}
- {{question.b|safe}}
+ {{question.b|safe}}
{{ form.b }}
{{ form.c.errors }}
- {{question.c|safe}}
+ {{question.c|safe}}
{{ form.c }}
{{ form.d.errors }}
- {{question.d|safe}}
+ {{question.d|safe}}
{{ form.d }}
{{ form.e.errors }}
- {{question.e|safe}}
+ {{question.e|safe}}
{{ form.e }}
@@ -118,14 +120,6 @@
$(document).ready(() => {
/* beautify ignore:start */
{% if not exam.publish_results %}
- //$("ul.physics-answer-list li").each((n, el) => {
- // $(el).click((e) => {
- // console.log(1, el.dataset.ans);
- // $("#id_answer").val(el.dataset.ans);
- // $("ul.physics-answer-list li").removeClass("selected");
- // $(el).addClass("selected")
- // })
- //})
let time_limit = '{{exam.time_limit}}'
if (time_limit != "None") {
diff --git a/physics/templates/physics/exam_take_overview.html b/physics/templates/physics/exam_take_overview.html
index 63a2fb04..c9123643 100644
--- a/physics/templates/physics/exam_take_overview.html
+++ b/physics/templates/physics/exam_take_overview.html
@@ -16,7 +16,7 @@
{% endif %} {% endcomment %}
- {{answer_count}} out of {{exam_length}} questions answered. Click to go to question.
+
{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.
{% for question, answer in question_answer_tuples %}
{% if request.user.is_authenticated %}
diff --git a/physics/tests/test_physics_exams.py b/physics/tests/test_physics_exams.py
new file mode 100644
index 00000000..6f5b3898
--- /dev/null
+++ b/physics/tests/test_physics_exams.py
@@ -0,0 +1,513 @@
+from http import HTTPStatus
+import json
+import os
+from re import A
+from django.conf import settings
+import pytest
+
+import tempfile
+
+# from django.contrib.auth.models import User
+from django.urls import reverse
+
+from rich.pretty import pprint
+
+from bs4 import BeautifulSoup
+
+from physics.views import GenericExamViews as PhysicsExamViews
+
+from physics.models import Category, CidUserAnswer, Exam, Question
+
+from generic.models import CidUser, CidUserGroup
+from rad.test_helpers import AssertNotFound
+
+
+
+APP_NAME = "physics"
+
+
+
+
+def create_cid_user_and_groups(db):
+ group1 = CidUserGroup.objects.create(name="Group1")
+ group2 = CidUserGroup.objects.create(name="Group2")
+
+ cid_user_1000 = CidUser.objects.create(cid=1000, passcode="ABCD", group=group1)
+ cid_user_1001 = CidUser.objects.create(cid=1001, passcode="EFGH", group=group1)
+ cid_user_2001 = CidUser.objects.create(cid=2001, passcode="EFGH", group=group2)
+
+ assert cid_user_1000.cid == 1000
+ assert cid_user_1001.cid == 1001
+
+
+def create_exam(db):
+ exam: Exam = PhysicsExamViews.Exam.objects.create(
+ name="Cid Exam", exam_mode=True, active=True
+ )
+
+ group1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
+
+ exam.cid_user_groups.add(group1)
+
+ exam.valid_cid_users.add(*group1.ciduser_set.all())
+
+ assert exam in group1.physics_cid_user_groups.all()
+
+ assert exam
+
+ return exam
+
+
+def create_category(db):
+ category = Category.objects.create(category="test category")
+ category.save()
+ category = Category.objects.create(category="test category 2")
+ category.save()
+
+
+def create_question(exam, answer=None):
+ # Just assign random stuff
+
+ if answer is None:
+ answers = {
+ "a_answer": True,
+ "b_answer": False,
+ "c_answer": True,
+ "d_answer": False,
+ "e_answer": True,
+ }
+ else:
+ answers = {
+ "a_answer": answer[0],
+ "b_answer": answer[1],
+ "c_answer": answer[2],
+ "d_answer": answer[3],
+ "e_answer": answer[4],
+ }
+
+ question: Question = Question.objects.create(
+ stem="test question stem",
+ a="question a text",
+ a_feedback="question a feedback",
+ b="question b text",
+ b_feedback="question b feedback",
+ c="question c text",
+ c_feedback="question c feedback",
+ d="question d text",
+ d_feedback="question d feedback",
+ e="question e text",
+ e_feedback="question e feedback",
+ **answers,
+ )
+ question.exams.set([exam])
+
+ return question
+
+
+def test_exams(db, client):
+ create_cid_user_and_groups(db)
+ exam = create_exam(db)
+
+ create_category(db)
+
+ questions = [create_question(exam), create_question(exam), create_question(exam)]
+
+ active_exams = client.get(reverse(f"{APP_NAME}:active_exams"))
+ assert active_exams.status_code == 200
+ assert len(json.loads(active_exams.content)["exams"]) == 0
+
+ cid_user_1001 = CidUser.objects.get(cid=1001)
+ cid_user_1000 = CidUser.objects.get(cid=1000)
+
+ for cid_user in [cid_user_1001, cid_user_1000]:
+ exam.active = True
+ exam.publish_results = False
+ exam.save()
+ # Test active exams sections works as intended
+ start_page_res = client.get(
+ reverse(f"physics:exam_start", kwargs={"pk": exam.pk})
+ )
+ start_page_soup = BeautifulSoup(start_page_res.content, "html.parser")
+ assert "Enter your CID and passcode" in str(start_page_soup)
+
+ for n, question in enumerate(questions):
+ cid_take_res = client.get(
+ reverse(
+ f"physics:exam_take",
+ kwargs={
+ "pk": exam.pk,
+ "sk": n,
+ "cid": cid_user.cid,
+ "passcode": cid_user.passcode,
+ },
+ )
+ )
+ cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
+
+ # Check correct user is displayed
+ assert (
+ str(cid_user.cid)
+ in cid_take_soup.find("span", {"id": "user-id"}).string
+ )
+
+ # Check review mode alert not visible
+ assert cid_take_soup.find("div", {"class": "review-mode-alert"}) is None
+
+ # Check that relevant buttons are being displayed
+ assert cid_take_soup.find("button", {"name": "finish"}) is not None
+
+ if n < len(questions) - 1:
+ assert cid_take_soup.find("button", {"name": "next"}) is not None
+
+ if n > 0:
+ assert cid_take_soup.find("button", {"name": "previous"}) is not None
+
+ assert cid_take_soup.find("span", {"id": "question-number"}).string == str(
+ n + 1
+ ) # + 1 for 0 offset
+ assert cid_take_soup.find("span", {"id": "exam-length"}).string == str(
+ len(questions)
+ )
+
+ # Check that stem text is displayed
+ assert (
+ cid_take_soup.find("span", {"id": "question-stem"}).string
+ == question.stem
+ )
+
+ assert cid_take_soup.find("label", {"for": "id_a"}).string == question.a
+ assert cid_take_soup.find("label", {"for": "id_b"}).string == question.b
+ assert cid_take_soup.find("label", {"for": "id_c"}).string == question.c
+ assert cid_take_soup.find("label", {"for": "id_d"}).string == question.d
+ assert cid_take_soup.find("label", {"for": "id_e"}).string == question.e
+
+ # As we haven't submitted any answers they should all be blank
+ assert (
+ cid_take_soup.find("input", {"type": "checkbox", "checked": True})
+ is None
+ )
+
+
+ cid_take_overview_res = client.get(
+ reverse(
+ f"physics:exam_take_overview",
+ kwargs={
+ "pk": exam.pk,
+ "cid": cid_user.cid,
+ "passcode": cid_user.passcode,
+ },
+ )
+ )
+ cid_take_overview_soup = BeautifulSoup(
+ cid_take_overview_res.content, "html.parser"
+ )
+ assert (
+ "0 out of 3 questions answered"
+ in cid_take_overview_soup.find(
+ "div", attrs={"class": "overview-text"}
+ ).string
+ )
+
+ assert (
+ len(
+ cid_take_overview_soup.find_all("button", attrs={"class": "unanswered"})
+ )
+ == 3
+ )
+
+ # Loop through questions again to test submissions
+ # All physics questions have 5 parts (and require a csrf token)
+ questions_len = len(questions) - 1
+ for n, question in enumerate(questions):
+ question_url = reverse(
+ f"physics:exam_take",
+ kwargs={
+ "pk": exam.pk,
+ "sk": n,
+ "cid": cid_user.cid,
+ "passcode": cid_user.passcode,
+ },
+ )
+ cid_take_res = client.get(question_url)
+ cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
+
+ # Extract csrftoken
+ csrftoken = cid_take_soup.find(
+ "input", attrs={"name": "csrfmiddlewaretoken"}
+ )["value"]
+ # Send answers (all TRUE)
+ post_json = {
+ "csrfmiddlewaretoken": csrftoken,
+ "a": "on",
+ "b": "on",
+ "c": "on",
+ "d": "on",
+ "e": "on",
+ "next": "",
+ }
+ res = client.post(question_url, data=post_json)
+
+ next_button = cid_take_soup.find("button", attrs={"name": "next"})
+
+ # Our final question will trigger a bad request (it shouldn't show a next button)
+ # We currently do still save the form though....
+ if questions_len == n:
+ assert next_button == None
+ assert res.status_code == HTTPStatus.BAD_REQUEST
+ else:
+ assert next_button
+ assert res.status_code == HTTPStatus.FOUND
+
+ overview_button = cid_take_soup.find("button", attrs={"name": "finish"})
+ assert overview_button
+
+ previous_button = cid_take_soup.find("button", attrs={"name": "previous"})
+
+ if n > 0:
+ assert previous_button
+ else:
+ assert not previous_button
+
+ # Filter answer to the question we just submitted to
+ cid_user_answers = question.cid_user_answers.all()
+
+ assert len(cid_user_answers) in (1, 2)
+
+ cid_user_answer = cid_user_answers.filter(cid=cid_user.cid)[0]
+
+ assert all(cid_user_answer.get_answers())
+
+
+ # Recheck the overview page (now that we have submitted answers)
+ cid_take_overview_res = client.get(
+ reverse(
+ f"physics:exam_take_overview",
+ kwargs={
+ "pk": exam.pk,
+ "cid": cid_user.cid,
+ "passcode": cid_user.passcode,
+ },
+ )
+ )
+ cid_take_overview_soup = BeautifulSoup(
+ cid_take_overview_res.content, "html.parser"
+ )
+ assert (
+ "3 out of 3 questions answered"
+ in cid_take_overview_soup.find(
+ "div", attrs={"class": "overview-text"}
+ ).string
+ )
+
+ assert (
+ len(
+ cid_take_overview_soup.find_all("button", attrs={"class": "unanswered"})
+ )
+ == 0
+ )
+
+ # Get overview scores page (invalid passcode)
+ AssertNotFound(client,
+ reverse(f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": "AAAA"})
+ )
+
+ # Get overview scores page
+ cid_scores_res = client.get(
+ reverse(f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode})
+ )
+
+ cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser")
+
+ search_exam = (
+ cid_scores_soup.find("div", {"id": "exam-assigned"})
+ .find("ul", {"class": exam.app_name})
+ .find_all("li", attrs={"data-exam-id": exam.pk})
+ )
+ assert search_exam
+ assert exam.name in str(search_exam)
+ assert "Active" in str(search_exam)
+ # assert "Active" in assigned_exams # check it is active
+
+ invalid_exam = cid_scores_soup.find_all(
+ "li", attrs={"data-exam-id": exam.pk + 5}
+ )
+ assert not invalid_exam # Check we can't find an invalid exam
+
+ # Check that we have a results link
+ results_exam = (
+ cid_scores_soup.find("div", {"id": "exam-results"})
+ .find("ul", {"class": exam.app_name})
+ .find_all("li", attrs={"data-exam-id": exam.pk})
+ )
+ assert results_exam
+ assert exam.name in str(results_exam)
+ assert exam.name + " test" not in str(results_exam)
+ assert "Results Published" not in str(
+ results_exam
+ ) # It should not be published yet
+
+
+ AssertNotFound(client,
+ reverse(
+ f"{exam.app_name}:exam_scores_cid_user",
+ kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": "AAAA"},
+ )
+ )
+
+ # Load the exam results page
+ cid_exam_scores_res = client.get(
+ reverse(
+ f"{exam.app_name}:exam_scores_cid_user",
+ kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode},
+ )
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ # Check that we have an alert that exams are not published
+ alert = cid_exam_scores_soup.find("div", {"class": "alert"})
+ assert "Results are not currently published." in str(alert)
+
+ # Check that we are showing enough questions / answers
+ answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"})
+ assert len(answer_lis) == 5 * len(questions)
+
+ submitted_answers = cid_exam_scores_soup.find_all(
+ "span", {"class": "submitted-user-answer"}
+ )
+ assert len(submitted_answers) == 5 * len(questions)
+
+ for ans in submitted_answers:
+ assert ans.text.strip() == "True"
+
+ # Check no correct answers are shown
+ assert not cid_exam_scores_soup.find_all("span", {"class": "correct-answer"})
+
+ # Add another question to the exam
+ extra_question = create_question(exam)
+ questions.append(extra_question)
+
+ # Load the exam results page (again)
+ cid_exam_scores_res = client.get(
+ reverse(
+ f"{exam.app_name}:exam_scores_cid_user",
+ kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode},
+ )
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"})
+ assert len(answer_lis) == 5 * len(questions)
+
+ submitted_answers = cid_exam_scores_soup.find_all(
+ "span", {"class": "submitted-user-answer"}
+ )
+ assert len(submitted_answers) == 5 * len(questions)
+
+ for ans in submitted_answers[:-5]:
+ assert ans.text.strip() == "True"
+
+ # The new question will not have any answers
+ for ans in submitted_answers[-5:]:
+ assert ans.text.strip() == ""
+
+ # Publish the results!
+ exam.publish_results = True
+ exam.save()
+
+ # Load the exam results page (again)
+ cid_exam_scores_res = client.get(
+ reverse(
+ f"{exam.app_name}:exam_scores_cid_user",
+ kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode},
+ )
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ assert (
+ cid_exam_scores_soup.find("div", {"class": "alert"}) is None
+ ) # Check our warning banner is gone
+
+ # Check that we are displaying the correct answers (correctly)
+ correct_answers = cid_exam_scores_soup.find_all(
+ "span", {"class": "correct-answer"}
+ )
+ assert len(correct_answers) == 5 * len(questions)
+
+ n = 0
+ for question in questions:
+ for ans in question.get_answers():
+ assert correct_answers[n].text.strip() == f"Correct answer: {ans}"
+ n = n + 1
+
+ submitted_answers = cid_exam_scores_soup.find_all(
+ "span", {"class": "submitted-user-answer"}
+ )
+ assert len(submitted_answers) == 5 * len(questions)
+
+ for ans in submitted_answers[:-5]:
+ assert ans.text.strip() == "True"
+
+ # The new question will not have any answers
+ for ans in submitted_answers[-5:]:
+ assert ans.text.strip() == ""
+
+ assert (
+ cid_exam_scores_soup.find("div", {"class": "score-overview"})
+ .find("span", {"id": "total-score"})
+ .string
+ == "9"
+ )
+ assert (
+ cid_exam_scores_soup.find("div", {"class": "score-overview"})
+ .find("span", {"id": "max-score"})
+ .string
+ == "20"
+ )
+
+ # answer the final question
+ cid_user_answer = CidUserAnswer(question=extra_question, a=True, b=False, c=True, d=False, e=True, cid=cid_user.cid, exam=exam)
+ cid_user_answer.save()
+
+ cid_exam_scores_res = client.get(
+ reverse(
+ f"{exam.app_name}:exam_scores_cid_user",
+ kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode},
+ )
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ # As we have answered correctly the new score should be increased by 5
+ assert (
+ cid_exam_scores_soup.find("div", {"class": "score-overview"})
+ .find("span", {"id": "total-score"})
+ .string
+ == "14"
+ )
+
+
+ # Check what happens when exam is not active
+ exam.active = False
+ exam.save()
+
+ start_page_res = client.get(
+ reverse(f"physics:exam_start", kwargs={"pk": exam.pk})
+ )
+ start_page_soup = BeautifulSoup(start_page_res.content, "html.parser")
+
+ assert "Exam is currently inactive" in start_page_soup.text
+
+ cid_take_res = client.get(
+ reverse(
+ f"physics:exam_take",
+ kwargs={
+ "pk": exam.pk,
+ "sk": 0,
+ "cid": cid_user.cid,
+ "passcode": cid_user.passcode,
+ },
+ )
+ )
+ cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
+ assert "Exam is currently inactive" in cid_take_soup.text
+
+ questions.pop(-1).delete()
\ No newline at end of file
diff --git a/physics/views.py b/physics/views.py
index 4716a105..39aa75cc 100644
--- a/physics/views.py
+++ b/physics/views.py
@@ -25,7 +25,7 @@ from django.core.cache import cache
from django.urls import reverse_lazy, reverse
-from django.http import Http404, JsonResponse
+from django.http import Http404, HttpResponseBadRequest, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse
from django_filters.views import FilterView
@@ -277,6 +277,13 @@ def exam_take(request, pk, sk, cid=None, passcode=None):
else:
answer = question.cid_user_answers.filter(user=request.user, exam=exam).first()
+ previous = -1
+ if sk > 0:
+ previous = sk - 1
+ next = sk + 1
+ if sk == exam_length - 1:
+ next = False
+
if request.method == "POST":
if answer:
form = CidUserAnswerForm(request.POST, instance=answer)
@@ -307,6 +314,8 @@ def exam_take(request, pk, sk, cid=None, passcode=None):
finish_url = "physics:exam_take_overview_user"
if "next" in request.POST:
+ if not next:
+ return HttpResponseBadRequest("Invalid request")
return redirect(take_url, sk=pos + 1, **kwargs)
elif "previous" in request.POST:
return redirect(take_url, sk=pos - 1, **kwargs)
@@ -317,12 +326,6 @@ def exam_take(request, pk, sk, cid=None, passcode=None):
else:
form = CidUserAnswerForm(instance=answer)
- previous = -1
- if sk > 0:
- previous = sk - 1
- next = sk + 1
- if sk == exam_length - 1:
- next = False
saved_answer = False
if answer is not None:
diff --git a/rad/test_helpers.py b/rad/test_helpers.py
new file mode 100644
index 00000000..03feb0cd
--- /dev/null
+++ b/rad/test_helpers.py
@@ -0,0 +1,6 @@
+from http import HTTPStatus
+
+def AssertNotFound(client, url):
+ """Helper to quickly test for urls that should return a 404 (NOT FOUND) error"""
+ response = client.get(url)
+ assert response.status_code == HTTPStatus.NOT_FOUND, f"Response content: {response.content}"
\ No newline at end of file
diff --git a/rapids/tests/test_rapids_exams.py b/rapids/tests/test_rapids_exams.py
new file mode 100644
index 00000000..a1a37e98
--- /dev/null
+++ b/rapids/tests/test_rapids_exams.py
@@ -0,0 +1,344 @@
+import json
+import os
+from re import A
+from django.conf import settings
+import pytest
+
+import tempfile
+
+# from django.contrib.auth.models import User
+from django.urls import reverse
+
+from rich.pretty import pprint
+
+from bs4 import BeautifulSoup
+
+from rapids.views import GenericExamViews as RapidExamViews
+
+from rapids.models import Answer, Exam, Examination, Rapid as Question
+
+from generic.models import CidUser, CidUserGroup
+
+APP_NAME = "rapids"
+
+
+def create_cid_user_and_groups(db):
+ group1 = CidUserGroup.objects.create(name="Group1")
+ group2 = CidUserGroup.objects.create(name="Group2")
+
+ cid_user_1000 = CidUser.objects.create(cid=1000, passcode="ABCD", group=group1)
+ cid_user_1001 = CidUser.objects.create(cid=1001, passcode="EFGH", group=group1)
+ cid_user_2001 = CidUser.objects.create(cid=2001, passcode="EFGH", group=group2)
+
+ assert cid_user_1000.cid == 1000
+ assert cid_user_1001.cid == 1001
+
+
+def create_exam(db):
+ exam: Exam = RapidExamViews.Exam.objects.create(
+ name="Cid Exam", exam_mode=True, active=True
+ )
+
+ group1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
+
+ exam.cid_user_groups.add(group1)
+
+ exam.valid_cid_users.add(*group1.ciduser_set.all())
+
+ assert exam in group1.rapids_cid_user_groups.all()
+
+ assert exam
+
+ return exam
+
+
+def create_examination(db):
+ examination = Examination.objects.create(examination="test examination")
+ examination.save()
+ examination = Examination.objects.create(examination="test examination 2")
+ examination.save()
+
+
+def create_rapid_image(db, rapid):
+ rapid_image = RapidImage()
+
+
+def create_question(exam, answer=None):
+ # Just assign random stuff
+
+ image = tempfile.NamedTemporaryFile(
+ dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False
+ )
+
+ question: Question = Question.objects.create(
+ laterality=Question.LATERALITY_CHOICES.NONE,
+ examination=Examination.objects.get(pk=1),
+ image=image.name,
+ )
+ question.exams.set([exam])
+
+ # Create an answer for the question
+ if answer is None:
+ answer_text = "testanswer"
+ else:
+ answer_text = answer
+ answer = Answer(question=question, answer=answer_text, answer_compare=answer_text, status=Answer.MarkOptions.CORRECT)
+ answer.save()
+
+ return question
+
+
+def test_exams(db, client):
+ return
+ create_cid_user_and_groups(db)
+ exam = create_exam(db)
+
+
+ create_structures(db)
+ create_question_types(db)
+
+ question1 = create_question(exam)
+ create_question(exam)
+ create_question(exam)
+
+ active_exams = client.get(reverse(f"{APP_NAME}:active_exams"))
+ assert active_exams.status_code == 200
+ assert len(json.loads(active_exams.content)["exams"]) == 0
+
+ cid_user_1001 = CidUser.objects.get(cid=1001)
+
+ for cid_user in [cid_user_1001]:
+ # Test active exams sections works as intended
+ active_exams_cid = client.get(
+ reverse(
+ f"active_exams_cid",
+ kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode},
+ )
+ )
+
+ assert active_exams_cid.status_code == 200
+ assert len(json.loads(active_exams_cid.content)["exams"]) == 1
+
+ active_exams_cid = client.get(
+ reverse(f"active_exams_cid", kwargs={"cid": 1001, "passcode": "EFGH"})
+ )
+
+ assert active_exams_cid.status_code == 200
+ assert len(json.loads(active_exams_cid.content)["exams"]) == 1
+
+ no_active_exams_cid = client.get(
+ reverse(f"active_exams_cid", kwargs={"cid": 2001, "passcode": "EGFH"})
+ )
+
+ assert no_active_exams_cid.status_code == 401
+ assert json.loads(no_active_exams_cid.content)["status"] == "invalid"
+
+ invalid_passcode = client.get(
+ reverse(f"active_exams_cid", kwargs={"cid": 1001, "passcode": "ABCD"})
+ )
+ assert invalid_passcode.status_code == 401
+ assert json.loads(invalid_passcode.content)["status"] == "invalid"
+
+ exam_metadata = json.loads(active_exams_cid.content)["exams"][0]
+
+ assert exam_metadata["name"] == "Cid Exam"
+ assert exam_metadata["exam_active"] == True
+ assert exam_metadata["exam_mode"] == True
+
+ res = client.get(exam_metadata["url"])
+ assert res.status_code == 200
+ exam_json = json.loads(res.content)
+
+ assert (
+ question1.get_question_json(answers=False)
+ in exam_json["questions"].values()
+ )
+
+ assert len(exam_json["questions"]) == 3
+
+ exam.active = False
+ exam.save()
+
+ res = client.get(exam_metadata["url"])
+ assert res.status_code == 404
+
+ exam.active = True
+ exam.save()
+
+ post_json = {
+ "eid": f"anatomy/{exam.pk}",
+ "cid": cid_user_1001.cid,
+ "start_time": "1659618141.731",
+ "answers": [[]],
+ }
+
+ # Test submission
+ # Start with 0 answers
+ res = client.post(reverse("global_exam_answers_submit"), data=post_json)
+
+ json_res = json.loads(res.content)
+
+ assert json_res["success"] == True
+ assert json_res["question_count"] == 0
+
+ post_json = {
+ "eid": f"anatomy/{exam.pk+10}",
+ "cid": cid_user_1001.cid,
+ "start_time": "1659618141.731",
+ "answers": [[]],
+ }
+ res = client.post(reverse("global_exam_answers_submit"), data=post_json)
+ json_res = json.loads(res.content)
+ assert json_res["success"] == False
+
+ # Test invalid cid
+ post_json = {
+ "eid": f"anatomy/{exam.pk+10}",
+ "cid": 1,
+ "start_time": "1659618141.731",
+ "answers": [[]],
+ }
+ res = client.post(reverse("global_exam_answers_submit"), data=post_json)
+ json_res = json.loads(res.content)
+ assert json_res["success"] == False
+ assert json_res["error"] == "Invalid data"
+
+ valid_answers = []
+
+ for questions in exam_json["questions"]:
+ for qid in questions:
+ valid_answers.append(
+ {
+ "eid": exam_json["eid"],
+ "cid": cid_user_1001.cid,
+ "passcode": cid_user_1001.passcode,
+ "qid": qid,
+ "qidn": "1",
+ "ans": f"answer {qid}",
+ }
+ )
+
+ post_json = {
+ "eid": exam_json["eid"],
+ "cid": cid_user_1001.cid,
+ "start_time": "1659618141.731",
+ "answers": json.dumps(valid_answers),
+ }
+
+ # Test submission
+ # give three answers
+ res = client.post(reverse("global_exam_answers_submit"), data=post_json)
+
+ json_res = json.loads(res.content)
+
+ assert json_res["success"] == True
+ assert json_res["question_count"] == 3
+
+ # We can then check the answers have been submitted (and can be viewed by the user)
+
+ # Get overview scores page
+ cid_scores_res = client.get(
+ reverse(f"cid_scores", kwargs={"cid": 1001, "passcode": "EFGH"})
+ )
+
+ cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser")
+
+ print(cid_scores_soup.find("div", {"id" : "exam-assigned"}))
+ search_exam = cid_scores_soup.find("div", {"id" : "exam-assigned"}).find("ul", {"class" : exam.app_name}).find_all("li", attrs={"data-exam-id": exam.pk})
+ assert search_exam
+ assert exam.name in str(search_exam)
+ assert "Active" in str(search_exam)
+ #assert "Active" in assigned_exams # check it is active
+
+ invalid_exam = cid_scores_soup.find_all("li", attrs={"data-exam-id": exam.pk+5})
+ assert not invalid_exam # Check we can't find an invalid exam
+
+ # Check that we have a results link
+ results_exam = cid_scores_soup.find("div", {"id" : "exam-results"}).find("ul", {"class" : exam.app_name}).find_all("li", attrs={"data-exam-id": exam.pk})
+ assert results_exam
+ assert exam.name in str(results_exam)
+ assert exam.name+" test" not in str(results_exam)
+ assert "Results Published" not in str(results_exam) # It should not be published yet
+
+
+ # Load the exam results page
+ cid_exam_scores_res = client.get(
+ reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"})
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ # Check that we have an alert that exams are not published
+ alert = cid_exam_scores_soup.find("div", {"class": "alert"})
+ assert "Results are not currently published." in str(alert)
+
+
+ answer_lis = cid_exam_scores_soup.find("ul", {"class": "score-answer-list"}).find_all("li")
+
+ assert len(answer_lis) == len(valid_answers)
+
+ for n in range(len(valid_answers)): # Check no answers are visible if results not published
+ answer = valid_answers[n]
+ li = answer_lis[n]
+
+ assert answer["ans"] in str(li) # Check that the saved answer is shown
+
+ assert li.find("span", {"class": "correct-answer"}).string == "*****" # and that the correct answer isn't
+
+ assert cid_exam_scores_soup.find("div", {"class": "score-overview"}) is None # check we hide the score overview if not published
+
+ # Add another question to the exam
+ create_question(exam)
+
+ # Load the exam results page (again)
+ cid_exam_scores_res = client.get(
+ reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"})
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ answer_lis = cid_exam_scores_soup.find("ul", {"class": "score-answer-list"}).find_all("li")
+ assert len(answer_lis) == len(valid_answers) + 1
+ assert "Not answered" in str(answer_lis[-1])
+
+ # Publish the results!
+ exam.publish_results = True
+ exam.save()
+
+ # Load the exam results page (again)
+ cid_exam_scores_res = client.get(
+ reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"})
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ assert cid_exam_scores_soup.find("div", {"class": "alert"}) is None # Check our warning banner is gone
+
+ answer_lis = cid_exam_scores_soup.find("ul", {"class": "score-answer-list"}).find_all("li")
+
+ assert len(answer_lis) == len(valid_answers) + 1
+ assert "Not answered" in str(answer_lis[-1])
+
+ # Repetition....
+ for n in range(len(valid_answers)): # Check answers are visible if results published
+ answer = valid_answers[n]
+ li = answer_lis[n]
+
+ assert answer["ans"] in str(li) # Check that the saved answer is shown
+
+ assert li.find("span", {"class": "correct-answer"}).string == "testanswer"
+
+ assert cid_exam_scores_soup.find("div", {"class": "score-overview"}).find("span", {"id": "total-score"}).string == "0 (3 unmarked)"
+
+
+ for question in exam.exam_questions.all():
+ try:
+ new_ans = Answer(question=question, answer=question.get_unmarked_user_answers().pop(), status=Answer.MarkOptions.CORRECT)
+ new_ans.save()
+ except KeyError: # Final question does not have an answer
+ pass
+
+ # Load the exam results page (again)
+ cid_exam_scores_res = client.get(
+ reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"})
+ )
+ cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
+
+ assert cid_exam_scores_soup.find("div", {"class": "score-overview"}).find("span", {"id": "total-score"}).string == "6"
\ No newline at end of file