Further basic integration testing

This commit is contained in:
Ross
2022-12-12 12:16:42 +00:00
parent a467f24e10
commit 73ebc838cd
9 changed files with 910 additions and 36 deletions
@@ -16,10 +16,15 @@
{% include 'user_score_header.html' %}
<ul class="score-answer-list">
{% for ans, score, correct_answer in answers_and_marks %}
<li class="user-answer-li" data-question-number="{{forloop.counter}}">Question {{forloop.counter}} - Correct answer: <span class="correct-answer">{{correct_answer}}</span>
<br/>
<span class="user-answer-score user-answer-score-{{score}} physics-ans">
<pre>{{ans}}</pre> ({{score}})
<li class="user-answer-li" data-question-number="{{forloop.counter}}">Question {{forloop.counter}} - Correct answer: {% if exam.publish_results %}<span class="correct-answer">{{correct_answer}}</span>{% endif %}
<br/>
<span class="user-answer-score user-answer-score-{{score}}">
<span class="submitted-user-answer">
<pre>{{ans}}</pre>
</span>
{% if exam.publish_results %}
{{score}}
{% endif %}
</span>
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
</li>
@@ -283,7 +283,7 @@ def test_exams(db, client):
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 not li.find("span", {"class": "correct-answer"}) # 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
@@ -5,13 +5,22 @@
{% include 'user_score_header.html' %}
<ul>
{% for question, ans in answers_and_marks %}
<li class="user-answer-li"><span class="question-stem-text">Question {{forloop.counter}} - {{ question.stem|safe }}</span></li>
<ol type="a">
{% for q, a, score, correct_answer in ans %}
<li><span class="question-text">{{q|safe}}:</span> Correct answer: {{correct_answer}} <br /><span class="submitted-user-answer">{{a}} <span class="answer-{{score}}">(Score:
{{score}})</span></span></li>
{% endfor %}
</ol>
<li class="user-answer-li"><span class="question-stem-text">Question {{forloop.counter}} - {{ question.stem|safe }}</span>
<ol type="a">
{% for q, a, score, correct_answer in ans %}
<li class="question-part">
<span class="question-text">{{q|safe}}:</span> {% if exam.publish_results %}<span class="correct-answer">Correct answer: {{correct_answer}}</span>{% endif %}
<br />
<span class="submitted-user-answer">{{a}}</span>
{% if exam.publish_results %}
<span class="answer-{{score}}">
(Score: {{score}})
</span>
{% endif %}
</li>
{% endfor %}
</ol>
</li>
{% endfor %}
</ul>
{% include 'user_scores_footer.html' %}
+10 -16
View File
@@ -1,11 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<span id="user-id">
{% if request.user.is_authenticated %}
User: {{request.user}}
{% else %}
CID: {{cid}}
{% endif %}
</span>
<div id="timeup">
Allocated time over.
</div>
@@ -29,15 +31,15 @@
</div>
</div>
<div class="no-select">
<h2>{{exam.name}}: Question [{{pos|add:1}}/{{exam_length}}]</h2>
<h2>{{exam.name}}: Question [<span id="question-number">{{pos|add:1}}</span>/<span id="exam-length">{{exam_length}}</span>]</h2>
{% if exam.publish_results %}
<div class="alert alert-primary" role="alert">
<div class="alert alert-primary review-mode-alert" role="alert">
Exam is in review mode. Add question feedback <a href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='physics' pk=question.pk %}')">here</a>.
</div>
{% endif %}
<div>
<p>{{question.stem|safe}}</p>
<p><span id="question-stem">{{question.stem|safe}}</span></p>
</div>
@@ -50,35 +52,35 @@
<li data-ans="a">
<div class="fieldWrapper flex-container">
{{ form.a.errors }}
<label for="{{ form.a.id_for_label }}" class="flex-8">{{question.a|safe}}</label>
<label for="{{ form.a.id_for_label }}" class="flex-8 question-text">{{question.a|safe}}</label>
<span class="flex-1">{{ form.a }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="b">
<div class="fieldWrapper flex-container">
{{ form.b.errors }}
<label for="{{ form.b.id_for_label }}" class="flex-8">{{question.b|safe}}</label>
<label for="{{ form.b.id_for_label }}" class="flex-8 question-text">{{question.b|safe}}</label>
<span class="flex-1">{{ form.b }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="c">
<div class="fieldWrapper flex-container">
{{ form.c.errors }}
<label for="{{ form.c.id_for_label }}" class="flex-8">{{question.c|safe}}</label>
<label for="{{ form.c.id_for_label }}" class="flex-8 question-text">{{question.c|safe}}</label>
<span class="flex-1">{{ form.c }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="d">
<div class="fieldWrapper flex-container">
{{ form.d.errors }}
<label for="{{ form.d.id_for_label }}" class="flex-8">{{question.d|safe}}</label>
<label for="{{ form.d.id_for_label }}" class="flex-8 question-text">{{question.d|safe}}</label>
<span class="flex-1">{{ form.d }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="e">
<div class="fieldWrapper flex-container">
{{ form.e.errors }}
<label for="{{ form.e.id_for_label }}" class="flex-8">{{question.e|safe}}</label>
<label for="{{ form.e.id_for_label }}" class="flex-8 question-text">{{question.e|safe}}</label>
<span class="flex-1">{{ form.e }}<span class="postinput"></span></span>
</div>
</li>
@@ -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") {
@@ -16,7 +16,7 @@
{% endif %} {% endcomment %}
<div><p>Questions</p></div>
{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.
<div class="overview-text">{{answer_count}} out of {{exam_length}} questions answered. Click to go to question.</div>
<div class="physics-finish-list">
{% for question, answer in question_answer_tuples %}
{% if request.user.is_authenticated %}
+513
View File
@@ -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()
+10 -7
View File
@@ -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:
+6
View File
@@ -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}"
+344
View File
@@ -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"