513 lines
17 KiB
Python
513 lines
17 KiB
Python
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() |