Files
penracourses/rad/tests/test_helpers.py
2024-08-05 13:20:29 +01:00

643 lines
23 KiB
Python

from http import HTTPStatus
import json
from typing import Callable
from bs4 import BeautifulSoup
import pytest
from generic.models import CidUser, CidUserGroup, UserUserGroup, ExamBase
from generic.views import ExamViews
from django.contrib.auth.models import User, Group
from django.urls import reverse
from rich.pretty import pprint
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}"
def create_cid_user_and_groups(db, marker=None):
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="IJKL", group=group2)
assert cid_user_1000.cid == 1000
assert cid_user_1001.cid == 1001
# Create UserUsers
user1 = User.objects.create_user(
username="user1", email="test@email.com", password="test1234"
)
user2 = User.objects.create_user(
username="user2", email="test2@email.com", password="test1234"
)
usergroup1 = UserUserGroup.objects.create(name="Group1")
usergroup1.users.add(user1)
usergroup2 = UserUserGroup.objects.create(name="Group2")
usergroup2.users.add(user2)
#if marker is not None:
# marker_group = Group.objects.create(name=f"{marker}_marker")
# #marker_group = Group.objects.create(name=f"{marker}_marker")
marker = User.objects.create_user(
username="marker", email="test@marker.com", password="test1234"
)
def create_exam(db, ExamViews: ExamViews):
exam: ExamBase = ExamViews.Exam.objects.create(
name="Cid Exam", exam_mode=True, active=True
)
cidgroup1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
usergroup1: UserUserGroup = UserUserGroup.objects.get(name="Group1")
exam.cid_user_groups.add(cidgroup1)
exam.valid_cid_users.add(*cidgroup1.ciduser_set.all())
# exam.valid_user_users.add(user1)
exam.valid_user_users.add(*usergroup1.users.all())
assert exam
return exam
class ExamTester:
def __init__(self, db, client, app_name, ExamViews) -> None:
self.db = db
self.client = client
self.app_name = app_name
create_cid_user_and_groups(db)
self.exam = exam = create_exam(db, ExamViews)
cidgroup1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
self.answers_per_question = 1
match app_name:
case "physics":
assert exam in cidgroup1.physics_cid_user_groups.all()
self.answers_per_question = 5
case "sbas":
assert exam in cidgroup1.sba_cid_user_groups.all()
cid_user_1001 = CidUser.objects.get(cid=1001)
cid_user_1000 = CidUser.objects.get(cid=1000)
self.cid_user_2001 = cid_user_2001 = CidUser.objects.get(cid=2001)
user1 = User.objects.get(username="user1")
user2 = User.objects.get(username="user2")
self.all_users = [cid_user_1000, cid_user_1001, cid_user_2001, user1, user2]
self.cid_users = [cid_user_1000, cid_user_1001, cid_user_2001]
self.invalid_users = [cid_user_2001, user2]
self.current_user = None
self.generated_questions = []
def load_user(self, user):
self.client.logout()
self.current_user = user
if isinstance(self.current_user, CidUser):
self.testing_cid_user = True
else:
self.testing_cid_user = False
self.client.force_login(user)
def create_questions(self, create_question_func: Callable):
self.create_question_func = create_question_func
self.generated_questions = [
create_question_func(self.exam),
create_question_func(self.exam),
create_question_func(self.exam),
]
def add_question(self):
self.generated_questions.append(self.create_question_func(self.exam))
def check_active_exams(self, n):
active_exams = self.client.get(reverse(f"{self.app_name}:active_exams"))
assert active_exams.status_code == 200
assert (
len(json.loads(active_exams.content)["exams"]) == n
), f"Current user: {self.current_user}"
def check_onsite_start_page(self):
start_page_res = self.client.get(
reverse(f"{self.app_name}:exam_start", kwargs={"pk": self.exam.pk})
)
# Check user access to the start page
assert start_page_res.status_code == HTTPStatus.OK
start_page_soup = BeautifulSoup(start_page_res.content, "html.parser")
if (
self.current_user in self.cid_users
or self.current_user in self.invalid_users
):
assert "Enter your CID and passcode" in str(start_page_soup)
else:
assert "Start Exam" in str(start_page_soup)
def check_take_page(self):
for n, question in enumerate(self.generated_questions):
if self.testing_cid_user:
take_url = reverse(
f"{self.app_name}:exam_take",
kwargs={
"pk": self.exam.pk,
"sk": n,
"cid": self.current_user.cid,
"passcode": self.current_user.passcode,
},
)
else:
take_url = reverse(
f"{self.app_name}:exam_take_user",
kwargs={
"pk": self.exam.pk,
"sk": n,
},
)
cid_take_res = self.client.get(take_url)
# Check that users that have not been added cannot access the take pages
if self.current_user in self.invalid_users:
assert cid_take_res.status_code == HTTPStatus.NOT_FOUND
continue
else:
assert (
cid_take_res.status_code == HTTPStatus.OK
), f"Current user: {self.current_user}"
cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
# Check correct user is displayed
if self.testing_cid_user:
display_user = str(self.current_user.cid)
else:
display_user = self.current_user.username
assert display_user 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(self.generated_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(self.generated_questions)
)
# Check that stem text is displayed
assert (
cid_take_soup.find("span", {"id": "question-stem"}).string
== question.stem
)
match self.app_name:
case "physics":
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
)
case "sbas":
assert (
cid_take_soup.find("ul", {"class": "sba-answer-list"})
is not None
)
assert (
len(
cid_take_soup.find(
"ul", {"class": "sba-answer-list"}
).find_all("li")
)
== 5
)
# As we haven't submitted any answers they should all be blank
assert (
cid_take_soup.find("input", {"type": "checkbox", "checked": True})
is None
)
def check_take_overview(self, answered, total):
if self.testing_cid_user:
take_overview_url = reverse(
f"{self.app_name}:exam_take_overview",
kwargs={
"pk": self.exam.pk,
"cid": self.current_user.cid,
"passcode": self.current_user.passcode,
},
)
else:
take_overview_url = reverse(
f"{self.app_name}:exam_take_overview_user",
kwargs={
"pk": self.exam.pk,
},
)
cid_take_overview_res = self.client.get(take_overview_url)
# Check that users that have not been added cannot access the take pages
if self.current_user in self.invalid_users:
assert cid_take_overview_res.status_code == HTTPStatus.NOT_FOUND
else:
assert cid_take_overview_res.status_code == HTTPStatus.OK
cid_take_overview_soup = BeautifulSoup(
cid_take_overview_res.content, "html.parser"
)
assert (
f"{answered} out of 3 questions answered"
in cid_take_overview_soup.find(
"div", attrs={"class": "overview-text"}
).text
)
assert (
len(
cid_take_overview_soup.find_all(
"button", attrs={"class": "unanswered"}
)
)
== total - answered
)
def check_cid_scores_page(self):
if self.testing_cid_user:
# Get overview scores page (invalid passcode)
AssertNotFound(
self.client,
reverse(
f"cid_scores",
kwargs={"cid": self.current_user.cid, "passcode": "AAAA"},
),
)
cid_scores_url = reverse(
f"cid_scores",
kwargs={
"cid": self.current_user.cid,
"passcode": self.current_user.passcode,
},
)
else:
cid_scores_url = reverse(f"user_scores")
# Get overview scores page
cid_scores_res = self.client.get(cid_scores_url)
assert cid_scores_res.status_code == HTTPStatus.OK
cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser")
if self.current_user in self.invalid_users:
# page should load but we shouldn't be able to find the exam
with pytest.raises(AttributeError):
search_exam = (
cid_scores_soup.find("div", {"id": "exam-assigned"})
.find("ul", {"class": self.exam.app_name.lower()})
.find_all("li", attrs={"data-exam-id": self.exam.pk})
)
else:
search_exam = (
cid_scores_soup.find("div", {"id": "exam-assigned"})
.find("ul", {"class": self.exam.app_name.lower()})
.find("li", attrs={"data-exam-id": self.exam.pk})
)
assert search_exam
assert str(self.exam) in str(search_exam)
assert len(search_exam.find("button", {"class": "start-button"})) > 0
# assert "Active" in assigned_exams # check it is active
invalid_exam = cid_scores_soup.find_all(
"li", attrs={"data-exam-id": self.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": self.exam.app_name})
.find_all("li", attrs={"data-exam-id": self.exam.pk})
)
assert results_exam
assert str(self.exam) in str(results_exam)
assert str(self.exam) + " test" not in str(results_exam)
assert "Results Published" not in str(
results_exam
) # It should not be published yet
def check_cid_scores_user_page(self):
if self.testing_cid_user:
AssertNotFound(
self.client,
reverse(
f"{self.exam.app_name}:exam_scores_cid_user",
kwargs={
"pk": self.exam.pk,
"cid": self.current_user.cid,
"passcode": "AAAA",
},
),
)
AssertNotFound(
self.client,
reverse(
f"{self.exam.app_name}:exam_scores_cid_user",
kwargs={
"pk": self.exam.pk,
"cid": self.cid_user_2001.cid,
"passcode": self.cid_user_2001.passcode,
},
),
)
exam_scores_url = reverse(
f"{self.exam.app_name}:exam_scores_cid_user",
kwargs={
"pk": self.exam.pk,
"cid": self.current_user.cid,
"passcode": self.current_user.passcode,
},
)
else:
exam_scores_url = reverse(
f"{self.exam.app_name}:exam_scores_user",
kwargs={"pk": self.exam.pk},
)
self.exam_scores_url = exam_scores_url
# Load the exam results page
cid_exam_scores_res = self.client.get(exam_scores_url)
if self.current_user in self.invalid_users:
assert cid_exam_scores_res.status_code == HTTPStatus.NOT_FOUND
return
else:
assert cid_exam_scores_res.status_code == HTTPStatus.OK
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)
submitted_answers = cid_exam_scores_soup.find_all(
"span", {"class": "submitted-user-answer"}
)
# Check that we are showing enough questions / answers
match self.app_name:
case "physics":
answer_lis = cid_exam_scores_soup.find_all(
"li", {"class": "question-part"}
)
assert len(answer_lis) == 5 * len(self.generated_questions)
assert len(submitted_answers) == 5 * len(self.generated_questions)
for ans in submitted_answers:
assert ans.text.strip() == "True"
case "sbas":
answer_lis = cid_exam_scores_soup.find_all(
"li", {"class": "user-answer-li"}
)
assert len(answer_lis) == len(self.generated_questions)
assert len(submitted_answers) == len(self.generated_questions)
for ans in submitted_answers:
assert ans.text.strip() == "a"
# Check no correct answers are shown
assert not cid_exam_scores_soup.find_all("span", {"class": "correct-answer"})
# Add another question to the exam
self.add_question()
# Load the exam results page (again)
cid_exam_scores_res = self.client.get(exam_scores_url)
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
submitted_answers = cid_exam_scores_soup.find_all(
"span", {"class": "submitted-user-answer"}
)
match self.app_name:
case "physics":
answer_lis = cid_exam_scores_soup.find_all(
"li", {"class": "question-part"}
)
assert len(answer_lis) == 5 * len(self.generated_questions)
assert len(submitted_answers) == 5 * len(self.generated_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() == "Not answered"
case "sbas":
answer_lis = cid_exam_scores_soup.find_all(
"li", {"class": "user-answer-li"}
)
assert len(answer_lis) == len(self.generated_questions)
assert len(submitted_answers) == len(self.generated_questions)
print(submitted_answers)
for ans in submitted_answers[:3]:
print("test1", ans)
assert ans.text.strip() == "a"
# The new question will not have any answers
for ans in submitted_answers[3:]:
print("test2", ans)
assert ans.text.strip() == "Not answered"
# Publish the results!
self.exam.publish_results = True
self.exam.save()
# Load the exam results page (again)
cid_exam_scores_res = self.client.get(exam_scores_url)
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) == self.answers_per_question * len(
self.generated_questions
)
n = 0
for i, question in enumerate(self.generated_questions):
match self.app_name:
case "physics":
for ans in question.get_answers():
assert correct_answers[n].text.strip() == f"Correct answer: {ans}"
n = n + 1
case "sbas":
ans = question.get_correct_answer()
assert correct_answers[i].text.strip() == f"Correct answer: {ans}"
submitted_answers = cid_exam_scores_soup.find_all(
"span", {"class": "submitted-user-answer"}
)
assert len(submitted_answers) == self.answers_per_question * len(
self.generated_questions
)
print(submitted_answers)
for ans in submitted_answers[:-self.answers_per_question]:
print("------", ans.text.strip())
match self.app_name:
case "physics":
assert ans.text.strip() == "True"
case "sbas":
assert ans.text.strip() == "a"
# The new question will not have any answers
for ans in submitted_answers[-self.answers_per_question:]:
assert ans.text.strip() == "Not answered"
match self.app_name:
case "physics":
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"
)
case "sbas":
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"})
.string
== "3"
)
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "max-score"})
.string
== "4"
)
def check_cid_scores_user_page2(self):
cid_exam_scores_res = self.client.get(self.exam_scores_url)
cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")
# As we have answered correctly the new score should be increased by 1 * answer_count
match self.app_name:
case "physics":
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"})
.string
== "14"
), f"Invalid answer count, Current user: {self.current_user}"
case "sbas":
assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"})
.string
== "4"
), f"Invalid answer count, Current user: {self.current_user}"
# Check what happens when exam is not active
self.exam.active = False
self.exam.save()
start_page_res = self.client.get(
reverse(f"{self.app_name}:exam_start", kwargs={"pk": self.exam.pk})
)
start_page_soup = BeautifulSoup(start_page_res.content, "html.parser")
assert "Exam is currently inactive" in start_page_soup.text
if self.testing_cid_user:
cid_take_url = reverse(
f"{self.app_name}:exam_take",
kwargs={
"pk": self.exam.pk,
"sk": 0,
"cid": self.current_user.cid,
"passcode": self.current_user.passcode,
},
)
else:
cid_take_url = reverse(
f"{self.app_name}:exam_take_user",
kwargs={
"pk": self.exam.pk,
"sk": 0,
},
)
cid_take_res = self.client.get(cid_take_url)
cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
print(cid_take_soup.text)
assert "Exam is currently inactive" in cid_take_soup.text
# Delete the last question, from both the list
q = self.generated_questions.pop()
# and the database
q.delete()