Further basic integration testing
This commit is contained in:
@@ -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' %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user