635 lines
23 KiB
Python
635 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
|
|
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):
|
|
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)
|
|
|
|
|
|
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()
|