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, UserAnswer, Exam, Question from generic.models import CidUser, CidUserGroup, UserUserGroup from rad.test_helpers import AssertNotFound, create_cid_user_and_groups from django.contrib.auth.models import User APP_NAME = "physics" 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") usergroup1: UserUserGroup = UserUserGroup.objects.get(name="Group1") exam.cid_user_groups.add(group1) exam.valid_cid_users.add(*group1.ciduser_set.all()) #exam.valid_user_users.add(user1) exam.valid_user_users.add(*usergroup1.users.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) generated_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) cid_user_2001 = CidUser.objects.get(cid=2001) user1 = User.objects.get(username="user1") users_tested = 1 for cid_user in [cid_user_1001, cid_user_1000, user1]: if isinstance(cid_user, CidUser): testing_cid_user = True else: testing_cid_user = False current_user = cid_user client.force_login(cid_user) exam.active = True exam.publish_results = 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") if testing_cid_user: assert "Enter your CID and passcode" in str(start_page_soup) else: assert "Start Exam" in str(start_page_soup) for n, question in enumerate(generated_questions): if testing_cid_user: take_url = reverse( f"physics:exam_take", kwargs={ "pk": exam.pk, "sk": n, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ) else: take_url = reverse( f"physics:exam_take_user", kwargs={ "pk": exam.pk, "sk": n, }, ) cid_take_res = client.get( take_url ) cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser") # Check correct user is displayed if testing_cid_user: display_user = str(cid_user.cid) else: display_user = 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(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(generated_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 ) if testing_cid_user: take_overview_url = reverse( f"physics:exam_take_overview", kwargs={ "pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ) else: take_overview_url = reverse( f"physics:exam_take_overview_user", kwargs={ "pk": exam.pk, }, ) cid_take_overview_res = client.get( take_overview_url ) 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(generated_questions) - 1 for n, question in enumerate(generated_questions): if testing_cid_user: question_url = reverse( f"physics:exam_take", kwargs={ "pk": exam.pk, "sk": n, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ) else: question_url = reverse( f"physics:exam_take_user", kwargs={ "pk": exam.pk, "sk": n, }, ) 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) == users_tested if testing_cid_user: cid_user_answer = cid_user_answers.filter(cid=cid_user.cid)[0] else: cid_user_answer = cid_user_answers.filter(user=current_user)[0] assert all(cid_user_answer.get_answers()) # Recheck the overview page (now that we have submitted answers) if testing_cid_user: take_overview_url = reverse( f"physics:exam_take_overview", kwargs={ "pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ) else: take_overview_url = reverse( f"physics:exam_take_overview_user", kwargs={ "pk": exam.pk, }, ) cid_take_overview_res = client.get( take_overview_url ) 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 ) if testing_cid_user: # Get overview scores page (invalid passcode) AssertNotFound(client, reverse(f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": "AAAA"}) ) cid_scores_url = reverse(f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode}) else: cid_scores_url = reverse(f"user_scores") # Get overview scores page cid_scores_res = client.get( cid_scores_url ) 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.lower()}) .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 if testing_cid_user: AssertNotFound(client, reverse( f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": "AAAA"}, ) ) AssertNotFound(client, reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": cid_user_2001.cid, "passcode": cid_user_2001.passcode}) ) exam_scores_url = reverse( f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode}, ) else: exam_scores_url = reverse( f"{exam.app_name}:exam_scores_user", kwargs={"pk": exam.pk}, ) # Load the exam results page cid_exam_scores_res = client.get( exam_scores_url ) 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(generated_questions) submitted_answers = cid_exam_scores_soup.find_all( "span", {"class": "submitted-user-answer"} ) assert len(submitted_answers) == 5 * len(generated_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) generated_questions.append(extra_question) # Load the exam results page (again) cid_exam_scores_res = client.get( exam_scores_url ) 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(generated_questions) submitted_answers = cid_exam_scores_soup.find_all( "span", {"class": "submitted-user-answer"} ) assert len(submitted_answers) == 5 * len(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() == "" # Publish the results! exam.publish_results = True exam.save() # Load the exam results page (again) cid_exam_scores_res = 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) == 5 * len(generated_questions) n = 0 for question in generated_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(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() == "" 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 if testing_cid_user: cid_user_answer = UserAnswer(question=extra_question, a=True, b=False, c=True, d=False, e=True, cid=cid_user.cid, exam=exam) else: cid_user_answer = UserAnswer(question=extra_question, a=True, b=False, c=True, d=False, e=True, user=current_user, exam=exam) cid_user_answer.save() cid_exam_scores_res = client.get( 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 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 if testing_cid_user: cid_take_url = reverse( f"physics:exam_take", kwargs={ "pk": exam.pk, "sk": 0, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ) else: cid_take_url = reverse( f"physics:exam_take_user", kwargs={ "pk": exam.pk, "sk": 0, }, ) cid_take_res = client.get( cid_take_url ) cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser") assert "Exam is currently inactive" in cid_take_soup.text generated_questions.pop(-1).delete() users_tested = users_tested + 1