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 anatomy.views import GenericExamViews as AnatomyExamViews from anatomy.models import ( Answer, Exam, AnatomyQuestion as Question, QuestionType, Structure, ) from generic.models import CidUser, CidUserGroup, UserUserGroup from rad.tests.test_helpers import AssertNotFound, create_cid_user_and_groups from django.contrib.auth.models import User APP_NAME = "anatomy" def create_exam(db): exam: Exam = AnatomyExamViews.Exam.objects.create( name="Cid Exam", exam_mode=True, active=True, open_access=False, exam_open_access=False ) 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(*usergroup1.users.all()) assert exam in group1.anatomy_cid_user_groups.all() assert exam return exam def create_structures(db): structure = Structure.objects.create(name="test structure") structure.save() structure = Structure.objects.create(name="test structure 2") structure.save() def create_question_types(db): question_type = QuestionType.objects.create(text="test question type") question_type.save() question_type = QuestionType.objects.create(text="test question type 2") question_type.save() def create_question(exam, answer=None): # Just assign random stuff image = tempfile.NamedTemporaryFile( dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False ) question: Question = Question.objects.create( question_type=QuestionType.objects.first(), structure=Structure.objects.first(), image=image.name, ) question.exams.set([exam]) # Create an answer for the question if answer is None: answer_text = "testanswer" else: answer_text = answer answer = Answer( question=question, answer=answer_text, answer_compare=answer_text, status=Answer.MarkOptions.CORRECT, ) answer.save() return question def test_exams(db, client): create_cid_user_and_groups(db) exam = create_exam(db) create_structures(db) create_question_types(db) question1 = create_question(exam) generated_questions = [question1, 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_1000 = CidUser.objects.get(cid=1000) cid_user_1001 = CidUser.objects.get(cid=1001) cid_user_2001 = CidUser.objects.get(cid=2001) user1 = User.objects.get(username="user1") # Test exam access urls # Check that we can't access without logging in assert client.get(exam.get_json_url()).status_code == 404 assert client.get(exam.get_json_url(cid=cid_user_2001.cid, passcode=cid_user_2001.passcode)).status_code == 404 # Valid user but incorrect passcode assert client.get(exam.get_json_url(cid=cid_user_1001.cid, passcode=cid_user_2001.passcode)).status_code == 404 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() # Test active exams sections works as intended if testing_cid_user: active_exams_url = reverse( f"active_exams_cid", kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode}, ) else: active_exams_url = reverse( f"active_exams", ) active_exams_cid = client.get(active_exams_url) assert active_exams_cid.status_code == 200 assert len(json.loads(active_exams_cid.content)["exams"]) == 1 if testing_cid_user: no_active_exams_cid = client.get( reverse(f"active_exams_cid", kwargs={"cid": 2001, "passcode": "EGFH"}) ) assert no_active_exams_cid.status_code == 401 assert json.loads(no_active_exams_cid.content)["status"] == "invalid" invalid_passcode = client.get( reverse( f"active_exams_cid", kwargs={"cid": cid_user.cid, "passcode": "AAAA"}, ) ) assert invalid_passcode.status_code == 401 assert json.loads(invalid_passcode.content)["status"] == "invalid" exam_metadata = json.loads(active_exams_cid.content)["exams"][0] assert exam_metadata["name"] == "Cid Exam" assert exam_metadata["exam_active"] == True assert exam_metadata["exam_mode"] == True # Check that we can access the exam json res = client.get(exam_metadata["url"]) assert res.status_code == 200 exam_json = json.loads(res.content) assert ( question1.get_question_json(answers=False) in exam_json["questions"].values() ) assert len(exam_json["questions"]) == 3 exam.active = False exam.save() res = client.get(exam_metadata["url"]) assert res.status_code == 404 exam.active = True exam.save() if testing_cid_user: post_cid = cid_user.cid else: post_cid = f"u-{current_user.pk}" post_json = { "eid": f"anatomy/{exam.pk}", "cid": post_cid, "start_time": "1659618141.731", "answers": [[]], } # Test submission # Start with 0 answers res = client.post(reverse("global_exam_answers_submit"), data=post_json) json_res = json.loads(res.content) assert json_res["success"] == True assert json_res["question_count"] == 0 post_json = { "eid": f"anatomy/{exam.pk+10}", "cid": post_cid, "start_time": "1659618141.731", "answers": [[]], } res = client.post(reverse("global_exam_answers_submit"), data=post_json) json_res = json.loads(res.content) assert json_res["success"] == False # Test invalid cid post_json = { "eid": f"anatomy/{exam.pk+10}", "cid": 1, "start_time": "1659618141.731", "answers": [[]], } res = client.post(reverse("global_exam_answers_submit"), data=post_json) json_res = json.loads(res.content) assert json_res["success"] == False assert json_res["error"] in ("Invalid data", "user / cid mismatch") valid_answers = [] for qid in exam_json["questions"]: post_data = { "eid": exam_json["eid"], "cid": post_cid, "qid": qid, "qidn": "1", "ans": f"answer {qid}", } if testing_cid_user: post_data["passcode"] = cid_user.passcode valid_answers.append(post_data) post_json = { "eid": exam_json["eid"], "cid": post_cid, "start_time": "1659618141.731", "answers": json.dumps(valid_answers), } # Test submission # give three answers res = client.post(reverse("global_exam_answers_submit"), data=post_json) json_res = json.loads(res.content) assert json_res["success"] == True assert json_res["question_count"] == 3 # We can then check the answers have been submitted (and can be viewed by the user) 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}) .find("li", attrs={"data-exam-id": exam.pk}) ) assert search_exam assert str(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": 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 str(exam) in str(results_exam) assert str(exam) + " 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) # Load the exam results page 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": "no-results"}) assert "Results are not currently published." in str(alert) answer_lis = cid_exam_scores_soup.find( "ul", {"class": "score-answer-list"} ).find_all("li") assert len(answer_lis) == len(valid_answers) for n in range( len(valid_answers) ): # Check no answers are visible if results not published answer = valid_answers[n] li = answer_lis[n] assert answer["ans"] in str(li) # Check that the saved answer is shown assert not li.find( "span", {"class": "correct-answer"} ) # and that the correct answer isn't assert ( cid_exam_scores_soup.find("div", {"class": "score-overview"}) is None ) # check we hide the score overview if not published # Add another question to the exam create_question(exam) # 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( "ul", {"class": "score-answer-list"} ).find_all("li") assert len(answer_lis) == len(valid_answers) + 1 assert "Not answered" in str(answer_lis[-1]) # 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 answer_lis = cid_exam_scores_soup.find( "ul", {"class": "score-answer-list"} ).find_all("li") assert len(answer_lis) == len(valid_answers) + 1 assert "Not answered" in str(answer_lis[-1]) # Repetition.... for n in range( len(valid_answers) ): # Check answers are visible if results published answer = valid_answers[n] li = answer_lis[n] assert answer["ans"] in str(li) # Check that the saved answer is shown assert li.find("span", {"class": "correct-answer"}).string == "testanswer" if users_tested == 1: # First user the answers will not have been saved assert ( cid_exam_scores_soup.find("div", {"class": "score-overview"}) .find("span", {"id": "total-score"}) .string == "0 (3 unmarked)" ) else: # Subsequent users should already have answers marked (apart from the last question which we have deleted) assert ( cid_exam_scores_soup.find("div", {"class": "score-overview"}) .find("span", {"id": "total-score"}) .string == "4 (1 unmarked)" ) for question in exam.exam_questions.all(): try: new_ans = Answer( question=question, answer=question.get_unmarked_user_answers().pop(), status=Answer.MarkOptions.CORRECT, ) new_ans.save() except KeyError: # Final question does not have an answer pass # 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": "score-overview"}) .find("span", {"id": "total-score"}) .string == "6" ) generated_questions.pop(-1).delete() users_tested = users_tested + 1