from collections import defaultdict 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 longs.views import GenericExamViews as LongsExamViews from longs.models import ( AnswerMarks, UserAnswer, Exam, Examination, Long as Question, LongSeries, LongSeriesImage, ) from generic.models import CidUser, CidUserGroup, UserUserGroup from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from rad.tests.test_helpers import AssertNotFound, create_cid_user_and_groups APP_NAME = "longs" def create_exam(db): exam: Exam = LongsExamViews.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(*usergroup1.users.all()) assert exam in group1.longs_cid_user_groups.all() assert exam return exam def create_examinations(db): examination = Examination.objects.create(examination="test examination") examination.save() examination = Examination.objects.create(examination="test examination 2") examination.save() def create_long_series(question, image_number=5): series = LongSeries( description="Test series", examination=Examination.objects.all().first(), ) series.save() for n in range(image_number): create_long_series_image(series, n) return series def create_long_series_image(series, n=0): image = tempfile.NamedTemporaryFile( dir=settings.MEDIA_ROOT, suffix="{n}.jpg", delete=False ) long_series_image = LongSeriesImage(image=image.name, series=series) long_series_image.save() def create_question(exam, answer=None, normal=False): # Just assign random stuff question: Question = Question.objects.create( description="Test question", history="Test history", feedback="Test feedback", model_observations="Model obs", model_interpretation="Model interp", model_principle_diagnosis="Model pd", model_differential_diagnosis="Model dd", model_management="Model management", ) series = create_long_series(question=question) question.series.add(series) question.exams.set([exam]) return question def test_exams(db, client): create_cid_user_and_groups(db) exam = create_exam(db) # create_structures(db) # create_question_types(db) create_examinations(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 = 0 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 res = client.get(exam_metadata["url"]) assert res.status_code == 200 exam_json = json.loads(res.content) # We can also check the unbased questions res = client.get(exam_metadata["url"] + "/unbased") assert res.status_code == 200 exam_json_unbased = json.loads(res.content) # This show we have different types (int vs string) assert ( set([str(i) for i in exam_json["exam_order"]]) == set(exam_json["questions"].keys()) == set(exam_json["question_requests"].keys()) == set([str(i) for i in exam_json_unbased["exam_order"]]) == set(exam_json_unbased["questions"].keys()) ) for q in exam_json["question_requests"]: # check redirects to the json file work # TODO: sort out testing of the saved JSON files try: question_json_res = client.get( reverse( f"longs:exam_question_json_cid", kwargs={ "pk": exam.pk, "sk": q, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ), follow=True, ) except AttributeError: # UserUser has no cid attribute question_json_res = client.get( reverse( f"longs:exam_question_json", kwargs={ "pk": exam.pk, "sk": q, }, ), follow=True, ) # assert question_json_res.status_code == 200 assert len(question_json_res.redirect_chain) == 2 assert question_json_res.redirect_chain[0][1] == 302 assert question_json_res.redirect_chain[1][1] == 302 assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json") if testing_cid_user: question_json_unbased_res = client.get( reverse( f"longs:exam_question_json_unbased_cid", kwargs={ "pk": exam.pk, "sk": q, "cid": cid_user.cid, "passcode": cid_user.passcode, }, ), follow=True, ) else: test = reverse( f"longs:exam_question_json_unbased", kwargs={ "pk": exam.pk, "sk": q, }, ) question_json_unbased_res = client.get( reverse( f"longs:exam_question_json_unbased", kwargs={ "pk": exam.pk, "sk": q, }, ), follow=True, ) assert question_json_unbased_res.status_code == 200 question_json = json.loads(question_json_unbased_res.content) # question_json_res = client.get(question1.get_question_json(answers=False, based=True)) # As we store references to the questions we have to check individually assert q in exam_json["questions"].keys() q_json = Question.objects.get(pk=q).get_question_json(based=False) assert question_json == q_json 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"long/{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"long/{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"long/{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"] == "Invalid data" # Long answers currently send a seperate object for each component valid_answers = [] valid_answers_extra = [] valid_answers_by_question = defaultdict(dict) sections = ( ("1", "answer observations"), ("2", "answer interpretation"), ("3", "answer principle diagnosis"), ("4", "answer differential diagnosis"), ("5", "answer management"), ) i = 0 for questions in exam_json["questions"]: for qid in questions: # Long answers submit 5 sections for n, name in sections: post_data = { "eid": exam_json["eid"], "cid": post_cid, "qid": qid, "qidn": n, "ans": f"{name} - {q}", } if testing_cid_user: post_data["passcode"] = cid_user.passcode valid_answers_extra.append(post_data) valid_answers_by_question[i][n] = post_data i = i + 1 post_json = { "eid": exam_json["eid"], "cid": post_cid, "start_time": "1659618141.731", "answers": json.dumps([*valid_answers, *valid_answers_extra]), } # 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"] is True assert json_res["question_count"] == 5 * 3 # Check the answers now exist in the database if testing_cid_user: answers = UserAnswer.objects.filter(cid=cid_user.cid) else: answers = UserAnswer.objects.filter(user=cid_user) assert answers.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": "alert"}) assert "Results are not currently published." in str(alert) answer_lis = cid_exam_scores_soup.find( "ul", {"class": "score-answer-list"} ).find_all("li", {"class": "user-answer-li"}) assert len(answer_lis) == len(valid_answers_by_question) for n in range( len(valid_answers_by_question) ): # Check no answers are visible if results not published lis = answer_lis[n] #n = str(n + 1) # questions are not zero indexed (lists are) for qidn, section_title in sections: answer = valid_answers_by_question[n][qidn] li = lis.find("li", attrs={"data-qidn": qidn}) assert li["class"][0].lower() in section_title 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", {"class": "user-answer-li"}) assert len(answer_lis) == len(valid_answers_by_question) + 1 assert "Not answered" in str(answer_lis[-1]) assert str(answer_lis[-1]).count("Not answered") == 5 # 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", {"class": "user-answer-li"}) assert len(answer_lis) == len(valid_answers_by_question) + 1 assert "Not answered" in str(answer_lis[-1]) assert str(answer_lis[-1]).count("Not answered") == 5 # Repetition.... for n in range( len(valid_answers_by_question) ): # Check no answers are visible if results not published lis = answer_lis[n] #n = str(n + 1) # questions are not zero indexed (lists are) for qidn, section_title in sections: answer = valid_answers_by_question[n][qidn] li = lis.find("li", attrs={"data-qidn": qidn}) assert li["class"][0].lower() in section_title assert answer["ans"] in str(li) # Check that the saved answer is shown # For long cases an unmarked answers is given a score of 4 # Each user needs marking individually assert ( cid_exam_scores_soup.find("div", {"class": "score-overview"}) .find("span", {"id": "total-score"}) .string == "3 (3 unmarked)" ) # Mark the questions # For longs this needs to be done for each user question: Question for question in exam.exam_questions.all(): answers = question.get_unmarked_user_answers() for answer in answers: answer.score = UserAnswer.ScoreOptions.EIGHT answer.save() #try: # new_ans = Answer( # question=question, # answer=question.get_unmarked_user_answers().pop(), # status=Answer.MarkOptions.CORRECT, # ) # new_ans.save() #except IndexError: # Final question does not have an answer # pass # TODO: test double marking # 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 == "27.0" ) generated_questions.pop(-1).delete() users_tested = users_tested + 1 assert users_tested == 3 # Some quick tests to check for user access to the marking page client.login(username="marker", password="test1234") # Load the mark overview page mark_overview_res = client.get(reverse( f"{exam.app_name}:mark_overview", kwargs={"pk": exam.pk}, )) assert mark_overview_res.status_code == 403 exam.markers.add(User.objects.get(username="marker")) mark_overview_res = client.get(reverse( f"{exam.app_name}:mark_overview", kwargs={"pk": exam.pk}, )) assert mark_overview_res.status_code == 200 mark_overview_soup = BeautifulSoup(mark_overview_res.content, "html.parser") to_mark = mark_overview_soup.find("ul", {"id": "question-mark-list"}).find_all("li") assert len(to_mark) == 3 for n in range(0,3): mark_question_res = client.get(reverse( f"{exam.app_name}:mark", kwargs={"exam_id": exam.pk, "sk": n}, )) assert mark_question_res.status_code == 200 mark_question_soup = BeautifulSoup(mark_question_res.content, "html.parser") print(mark_question_soup) #mark_links = mark_question_soup.find_all("a", {"class": "mark-answer-link"}) #assert len(mark_links) == 2 #for link in mark_links: # mark_res = client.get(link["href"]) # assert mark_res.status_code == 200 mark_question_res = client.get(reverse( f"{exam.app_name}:mark", kwargs={"exam_id": exam.pk, "sk": 4}, )) assert mark_question_res.status_code == 404 mark_question_soup = BeautifulSoup(mark_question_res.content, "html.parser")