From 0e94f780ff46e8ec0b6a158861052ccf2b82591b Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 23 Jan 2023 12:56:33 +0000 Subject: [PATCH] basic rapid integration testing --- Dockerfile | 2 +- anatomy/models.py | 4 +- atlas/migrations/0048_auto_20230116_1119.py | 33 ++ generic/migrations/0049_auto_20230116_1119.py | 13 + .../generic/exam_overview_headers.html | 5 +- generic/views.py | 5 +- rapids/models.py | 86 ++-- rapids/templates/rapids/exam_scores_user.html | 17 +- rapids/tests/test_rapids_exams.py | 383 +++++++++++++----- 9 files changed, 400 insertions(+), 148 deletions(-) create mode 100644 atlas/migrations/0048_auto_20230116_1119.py create mode 100644 generic/migrations/0049_auto_20230116_1119.py diff --git a/Dockerfile b/Dockerfile index 2d971152..4494b92c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # pull official base image -FROM python:3.10.4-slim +FROM python:3.11.1-slim # set work directory WORKDIR /usr/src/rad diff --git a/anatomy/models.py b/anatomy/models.py index 73898b16..a2b6af06 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -265,13 +265,11 @@ class AnatomyQuestion(models.Model): ] ) - def get_question_json(self, based=True, feedback=False, answers=True): + def get_question_json(self, based: bool=True, answers: bool=True): """Returns a json representation of the question""" - # Loop through rapidimage associations images = [] annotations = [] - feedback_images = [] if based: images.append(image_as_base64(self.image)) diff --git a/atlas/migrations/0048_auto_20230116_1119.py b/atlas/migrations/0048_auto_20230116_1119.py new file mode 100644 index 00000000..1a35085d --- /dev/null +++ b/atlas/migrations/0048_auto_20230116_1119.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.13 on 2023-01-16 11:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0047_alter_condition_parent_alter_condition_synonym_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='condition', + name='parent', + field=models.ManyToManyField(blank=True, related_name='_atlas_condition_parent_+', to='atlas.Condition'), + ), + migrations.AlterField( + model_name='condition', + name='synonym', + field=models.ManyToManyField(blank=True, related_name='_atlas_condition_synonym_+', to='atlas.Condition'), + ), + migrations.AlterField( + model_name='finding', + name='synonym', + field=models.ManyToManyField(blank=True, related_name='_atlas_finding_synonym_+', to='atlas.Finding'), + ), + migrations.AlterField( + model_name='structure', + name='synonym', + field=models.ManyToManyField(blank=True, related_name='_atlas_structure_synonym_+', to='atlas.Structure'), + ), + ] diff --git a/generic/migrations/0049_auto_20230116_1119.py b/generic/migrations/0049_auto_20230116_1119.py new file mode 100644 index 00000000..f1c2700d --- /dev/null +++ b/generic/migrations/0049_auto_20230116_1119.py @@ -0,0 +1,13 @@ +# Generated by Django 3.2.13 on 2023-01-16 11:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0048_alter_userusergroup_archive_alter_userusergroup_name_and_more'), + ] + + operations = [ + ] diff --git a/generic/templates/generic/exam_overview_headers.html b/generic/templates/generic/exam_overview_headers.html index 6b898807..c1e3f82e 100644 --- a/generic/templates/generic/exam_overview_headers.html +++ b/generic/templates/generic/exam_overview_headers.html @@ -37,7 +37,10 @@ This exam has {{question_number}} questions. Time limit: {{exam.time_limit}} seconds. Exam mode: {{ exam.exam_mode }}
-Cid candidates: {{candidate_count.0}}, User candidates: {{candidate_count.1}}
+ +{% if exam.exam_mode %} + Cid candidates: {{candidate_count.0}}, User candidates: {{candidate_count.1}}
+{% endif %} Open access: {{ exam.open_access }}
diff --git a/generic/views.py b/generic/views.py index d13e812f..6bd25391 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1371,6 +1371,7 @@ class ExamViews(View, LoginRequiredMixin): n = 0 for answer in json.loads(request.POST.get("answers")): + print("ANSWER", answer) eid = int(answer["eid"].split("/")[1]) uid = False @@ -1454,7 +1455,9 @@ class ExamViews(View, LoginRequiredMixin): ans = existing_answers[0] if answer["qidn"] == "1": ans.normal = normal - ans.answer = "" + # Only wipe the answer text if the new answer is "Normal" + if normal: + ans.answer = "" elif answer["qidn"] == "2" and not ans.normal: ans.answer = posted_answer case "anatomy": diff --git a/rapids/models.py b/rapids/models.py index 5e74dfcf..1db86d4e 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -48,6 +48,20 @@ import hashlib import pydicom import pydicom.errors +from typing import List, TypedDict, Required, NotRequired + + +class QuestionData(TypedDict): + images: Required[list[str]] + annotations: Required[List[str]] + type: Required[str] + history: NotRequired[str] + normal: Required[bool] + answers: NotRequired[List[str]] + feedback: NotRequired[str] + feedback_images: NotRequired[List[str]] + + image_storage = FileSystemStorage( # Physical file location ROOT location="{0}".format(settings.MEDIA_ROOT), @@ -136,6 +150,15 @@ class Answer(models.Model): class Rapid(models.Model): + """ + Django model that defines a rapid question. + + Images are help by a seperate model: RapidImage + which has a foreign key to this model + + + """ + question = models.TextField(null=True, blank=True) history = models.TextField(null=True, blank=True) feedback = models.TextField(null=True, blank=True) @@ -293,7 +316,7 @@ class Rapid(models.Model): exams = ", ".join(e) return exams - def get_unmarked_user_answer_string(self, exam_pk=None): + def get_unmarked_user_answer_string(self, exam_pk: int=None): unmarked_answers = self.get_unmarked_user_answers(exam_pk) if not unmarked_answers: @@ -304,7 +327,7 @@ class Rapid(models.Model): ) ) - def get_unmarked_user_answers(self, exam_pk=None, marker=None): + def get_unmarked_user_answers(self, exam_pk: int|None=None, marker=None): # If normal no answers to mark (they will be automarked) if self.normal: return [] @@ -446,8 +469,6 @@ class Rapid(models.Model): answers.append("{} {}".format(a.name, laterality)) answers.append("{}".format(a.name)) - - compare_answers = self.get_compare_answers() new_answers = [ @@ -456,24 +477,30 @@ class Rapid(models.Model): return new_answers - def get_question_json(self, based=True, feedback=False): - """Returns json""" + def get_question_json( + self, based: bool = True, feedback: bool = False, answers: bool = True, history: bool = False, annotations: bool = False + ) -> QuestionData: + """Returns a json representation of the question""" # Loop through rapidimage associations images = [] annotations = [] feedback_images = [] for i in self.images.all(): - annotations.append(i.image_annotations) + # TODO: add anotations + if i.feedback_image == True: - if based: - feedback_images.append(image_as_base64(i.image)) - else: - feedback_images.append( - "{}/{}".format(settings.REMOTE_URL, i.image.url) - ) - # feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url)) + if feedback: + annotations.append(i.image_annotations) + if based: + feedback_images.append(image_as_base64(i.image)) + else: + feedback_images.append( + "{}/{}".format(settings.REMOTE_URL, i.image.url) + ) + # feedback_images.append("{}/{}".format(settings.REMOTE_URL, i.image.url)) else: + annotations.append(i.image_annotations) if based: images.append(image_as_base64(i.image)) else: @@ -482,16 +509,21 @@ class Rapid(models.Model): json = { "images": images, # "feedback_image": [], - "annotations": annotations, "type": "rapid", } - json["history"] = self.history - json["normal"] = self.normal - json["feedback_images"] = feedback_images - json["answers"] = list(self.get_correct_unstripped_answers()) + if annotations: + json["annotations"] = self.annotations + + if history: + json["history"] = self.history + + if answers: + json["normal"] = self.normal + json["answers"] = list(self.get_correct_unstripped_answers()) if feedback: + json["feedback_images"] = feedback_images json["feedback"] = self.feedback return json @@ -537,8 +569,6 @@ class RapidImage(models.Model): is_dicom = models.BooleanField(default=False) - - def image_tag(self): if self.image: return mark_safe( @@ -580,8 +610,12 @@ class RapidImage(models.Model): self.is_dicom = True except pydicom.errors.InvalidDicomError: - self.image.file.open() - hash = hashlib.md5(self.image.read()).hexdigest() + try: # This is horrible (but needed for current unit tests) + # (we use a temporary file that breaks here) + self.image.file.open() + hash = hashlib.md5(self.image.read()).hexdigest() + except AttributeError: + return self.image_md5_hash = hash super().save(*args, **kwargs) # Call the "real" save() method. @@ -633,15 +667,14 @@ class Exam(ExamBase): CidUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", - related_name="rapid_cid_user_groups" - + related_name="rapid_cid_user_groups", ) user_user_groups = models.ManyToManyField( UserUserGroup, blank=True, help_text="These groups define which candidates are able to be added to the exams/collection.", - related_name="rapid_user_user_groups" + related_name="rapid_user_user_groups", ) exam_user_status = GenericRelation(ExamUserStatus) @@ -667,6 +700,7 @@ class Exam(ExamBase): exam_order = [] + q: Rapid for q in questions: exam_order.append(q.id) diff --git a/rapids/templates/rapids/exam_scores_user.html b/rapids/templates/rapids/exam_scores_user.html index b6ad62b8..2157b7fa 100644 --- a/rapids/templates/rapids/exam_scores_user.html +++ b/rapids/templates/rapids/exam_scores_user.html @@ -19,11 +19,18 @@ {% include 'user_score_header.html' %} {% include 'user_scores_footer.html' %} diff --git a/rapids/tests/test_rapids_exams.py b/rapids/tests/test_rapids_exams.py index a1a37e98..ade83ad1 100644 --- a/rapids/tests/test_rapids_exams.py +++ b/rapids/tests/test_rapids_exams.py @@ -15,37 +15,31 @@ from bs4 import BeautifulSoup from rapids.views import GenericExamViews as RapidExamViews -from rapids.models import Answer, Exam, Examination, Rapid as Question +from rapids.models import Answer, CidUserAnswer, Exam, Examination, Rapid as Question, RapidImage -from generic.models import CidUser, CidUserGroup +from generic.models import CidUser, CidUserGroup, UserUserGroup + +from django.contrib.auth.models import User + +from rad.test_helpers import AssertNotFound, create_cid_user_and_groups APP_NAME = "rapids" -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="EFGH", group=group2) - - assert cid_user_1000.cid == 1000 - assert cid_user_1001.cid == 1001 - - def create_exam(db): exam: Exam = RapidExamViews.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.rapids_cid_user_groups.all() + assert exam in group1.rapid_cid_user_groups.all() assert exam @@ -59,22 +53,29 @@ def create_examination(db): examination.save() -def create_rapid_image(db, rapid): - rapid_image = RapidImage() - - -def create_question(exam, answer=None): - # Just assign random stuff - +def create_rapid_image(question): image = tempfile.NamedTemporaryFile( dir=settings.MEDIA_ROOT, suffix=".jpg", delete=False ) + rapid_image = RapidImage(image=image, rapid=question) + + rapid_image.save() + + +def create_question(exam, answer=None, normal=False): + # Just assign random stuff + question: Question = Question.objects.create( - laterality=Question.LATERALITY_CHOICES.NONE, - examination=Examination.objects.get(pk=1), - image=image.name, + laterality=Question.NONE, + # examination=Examination.objects.get(pk=1), + # image=imagohe.name, + history="TEST history", + normal=normal, ) + + create_rapid_image(question=question) + question.exams.set([exam]) # Create an answer for the question @@ -82,62 +83,101 @@ def create_question(exam, answer=None): answer_text = "testanswer" else: answer_text = answer - answer = Answer(question=question, answer=answer_text, answer_compare=answer_text, status=Answer.MarkOptions.CORRECT) + answer = Answer( + question=question, + answer=answer_text, + answer_compare=answer_text, + status=Answer.MarkOptions.CORRECT, + ) answer.save() return question def test_exams(db, client): - return create_cid_user_and_groups(db) exam = create_exam(db) - - create_structures(db) - create_question_types(db) + # create_structures(db) + # create_question_types(db) + create_examination(db) question1 = create_question(exam) - create_question(exam) - 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() - for cid_user in [cid_user_1001]: # Test active exams sections works as intended - active_exams_cid = client.get( - reverse( + 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) + + print(cid_user) assert active_exams_cid.status_code == 200 assert len(json.loads(active_exams_cid.content)["exams"]) == 1 - active_exams_cid = client.get( - reverse(f"active_exams_cid", kwargs={"cid": 1001, "passcode": "EFGH"}) - ) + if testing_cid_user: + no_active_exams_cid = client.get( + reverse(f"active_exams_cid", kwargs={"cid": 2001, "passcode": "EGFH"}) + ) - assert active_exams_cid.status_code == 200 - assert len(json.loads(active_exams_cid.content)["exams"]) == 1 + assert no_active_exams_cid.status_code == 401 + assert json.loads(no_active_exams_cid.content)["status"] == "invalid" - 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": 1001, "passcode": "ABCD"}) - ) - assert invalid_passcode.status_code == 401 - assert json.loads(invalid_passcode.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] @@ -148,6 +188,7 @@ def test_exams(db, client): res = client.get(exam_metadata["url"]) assert res.status_code == 200 exam_json = json.loads(res.content) + print(exam_json) assert ( question1.get_question_json(answers=False) @@ -165,9 +206,14 @@ def test_exams(db, client): 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": cid_user_1001.cid, + "eid": f"rapid/{exam.pk}", + "cid": post_cid, "start_time": "1659618141.731", "answers": [[]], } @@ -182,8 +228,8 @@ def test_exams(db, client): assert json_res["question_count"] == 0 post_json = { - "eid": f"anatomy/{exam.pk+10}", - "cid": cid_user_1001.cid, + "eid": f"rapid/{exam.pk+10}", + "cid": post_cid, "start_time": "1659618141.731", "answers": [[]], } @@ -193,7 +239,7 @@ def test_exams(db, client): # Test invalid cid post_json = { - "eid": f"anatomy/{exam.pk+10}", + "eid": f"rapid/{exam.pk+10}", "cid": 1, "start_time": "1659618141.731", "answers": [[]], @@ -203,26 +249,39 @@ def test_exams(db, client): assert json_res["success"] == False assert json_res["error"] == "Invalid data" + + # Rapid answers currently send a seperate object for each component valid_answers = [] + valid_answers_extra = [] for questions in exam_json["questions"]: for qid in questions: - valid_answers.append( - { - "eid": exam_json["eid"], - "cid": cid_user_1001.cid, - "passcode": cid_user_1001.passcode, - "qid": qid, - "qidn": "1", - "ans": f"answer {qid}", - } - ) + post_data = { + "eid": exam_json["eid"], + "cid": post_cid, + "qid": qid, + "qidn": "1", + "ans": f"Abnormal", + } + post_data2 = { + "eid": exam_json["eid"], + "cid": post_cid, + "qid": qid, + "qidn": "2", + "ans": f"answer {qid}", + } + if testing_cid_user: + post_data["passcode"] = cid_user.passcode + post_data2["passcode"] = cid_user.passcode + + valid_answers_extra.append(post_data) + valid_answers.append(post_data2) post_json = { "eid": exam_json["eid"], - "cid": cid_user_1001.cid, + "cid": post_cid, "start_time": "1659618141.731", - "answers": json.dumps(valid_answers), + "answers": json.dumps([*valid_answers, *valid_answers_extra]), } # Test submission @@ -232,70 +291,142 @@ def test_exams(db, client): json_res = json.loads(res.content) assert json_res["success"] == True - assert json_res["question_count"] == 3 + assert json_res["question_count"] == 6 + + # Check the answers now exist in the database + if testing_cid_user: + answers = CidUserAnswer.objects.filter(cid=cid_user.cid) + else: + answers = CidUserAnswer.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( - reverse(f"cid_scores", kwargs={"cid": 1001, "passcode": "EFGH"}) - ) + cid_scores_res = client.get(cid_scores_url) cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser") - print(cid_scores_soup.find("div", {"id" : "exam-assigned"})) - search_exam = cid_scores_soup.find("div", {"id" : "exam-assigned"}).find("ul", {"class" : exam.app_name}).find_all("li", attrs={"data-exam-id": exam.pk}) + search_exam = ( + cid_scores_soup.find("div", {"id": "exam-assigned"}) + .find("ul", {"class": exam.app_name}) + .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 + # 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 + 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}) + 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 + 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( - reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"}) - ) + 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") + 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 + 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 answer["ans"] in str(li) # Check that the saved answer is shown - assert li.find("span", {"class": "correct-answer"}).string == "*****" # and that the correct answer isn't + 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 + 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( - reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"}) - ) + 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") + 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]) @@ -304,41 +435,71 @@ def test_exams(db, client): exam.save() # Load the exam results page (again) - cid_exam_scores_res = client.get( - reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"}) - ) + 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 + 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") + 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 + 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 answer["ans"] in str(li) # Check that the saved answer is shown assert li.find("span", {"class": "correct-answer"}).string == "testanswer" - assert cid_exam_scores_soup.find("div", {"class": "score-overview"}).find("span", {"id": "total-score"}).string == "0 (3 unmarked)" - + 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(): + print(question) + print(question.get_compare_answers()) try: - new_ans = Answer(question=question, answer=question.get_unmarked_user_answers().pop(), status=Answer.MarkOptions.CORRECT) + 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 + except IndexError: # Final question does not have an answer pass # Load the exam results page (again) - cid_exam_scores_res = client.get( - reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": 1001, "passcode": "EFGH"}) - ) + 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" \ No newline at end of file + 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