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 # The main take page loads the question controls via HTMX/JS into # the fragment URL. Request that fragment and run button/answer # checks against it (server-rendered), not the main page which is # just a container. if self.testing_cid_user: frag_url = reverse( f"{self.app_name}:exam_take_fragment", kwargs={ "pk": self.exam.pk, "sk": n, "cid": self.current_user.cid, "passcode": self.current_user.passcode, }, ) else: frag_url = reverse( f"{self.app_name}:exam_take_fragment_user", kwargs={ "pk": self.exam.pk, "sk": n, }, ) frag_res = self.client.get(frag_url) # If the user is invalid they may not be able to load the fragment if self.current_user in self.invalid_users: assert frag_res.status_code == HTTPStatus.NOT_FOUND continue else: assert frag_res.status_code == HTTPStatus.OK frag_soup = BeautifulSoup(frag_res.content, "html.parser") # Check that relevant buttons are being displayed in the fragment assert frag_soup.find("button", {"name": "finish"}) is not None if n < len(self.generated_questions) - 1: assert frag_soup.find("button", {"name": "next"}) is not None if n > 0: assert frag_soup.find("button", {"name": "previous"}) is not None # Check that stem text is displayed (main page header is also kept in # sync by fragment JS, but verify here that fragment contains stem) assert ( frag_soup.find("div", {"class": "question-stem-fragment"}).string.strip() == question.stem ) match self.app_name: case "physics": assert ( frag_soup.find("label", {"for": "id_a"}).string == question.a ) assert ( frag_soup.find("label", {"for": "id_b"}).string == question.b ) assert ( frag_soup.find("label", {"for": "id_c"}).string == question.c ) assert ( frag_soup.find("label", {"for": "id_d"}).string == question.d ) assert ( frag_soup.find("label", {"for": "id_e"}).string == question.e ) case "sbas": assert ( frag_soup.find("ul", {"class": "sba-answer-list"}) is not None ) assert ( len( frag_soup.find( "ul", {"class": "sba-answer-list"} ).find_all("li") ) == 5 ) # As we haven't submitted any answers they should all be blank assert ( frag_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 ) # Different apps render unanswered markers differently: # - `sbas` uses