From afdc8e95c75da1fa9e6d5a8148a96117445d3a97 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 5 Jun 2023 16:44:35 +0100 Subject: [PATCH] fix longs tests --- generic/urls.py | 28 ++- generic/views.py | 22 +- longs/models.py | 25 ++- longs/templates/longs/exam_scores_user.html | 114 +++++----- longs/tests/test_longs_exams.py | 226 +++++++++++++------- templates/user_score_header.html | 15 +- 6 files changed, 278 insertions(+), 152 deletions(-) diff --git a/generic/urls.py b/generic/urls.py index 71152f1e..c059ae17 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -286,25 +286,45 @@ def generic_exam_urls(generic_exam_view: GenericExamViews): ), # These are a little more ambiguous than ideal path( - "exam/json//", + "exam/json//question/", generic_exam_view.exam_question_json, name="exam_question_json", ), path( - "exam/json///unbased", + "exam/json//question//unbased", generic_exam_view.exam_question_json_unbased, name="exam_question_json_unbased", ), path( - "exam/json////", + "exam/json//question///", generic_exam_view.exam_question_json_cid, name="exam_question_json_cid", ), path( - "exam/json/////unbased", + "exam/json//question////unbased", generic_exam_view.exam_question_json_unbased_cid, name="exam_question_json_unbased_cid", ), + #path( + # "exam/json//", + # generic_exam_view.exam_question_json, + # name="exam_question_json", + #), + #path( + # "exam/json///unbased", + # generic_exam_view.exam_question_json_unbased, + # name="exam_question_json_unbased", + #), + #path( + # "exam/json////", + # generic_exam_view.exam_question_json_cid, + # name="exam_question_json_cid", + #), + #path( + # "exam/json/////unbased", + # generic_exam_view.exam_question_json_unbased_cid, + # name="exam_question_json_unbased_cid", + #), path( "exam/json//recreate", generic_exam_view.exam_json_recreate, diff --git a/generic/views.py b/generic/views.py index b1e45a94..a979fb87 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1587,6 +1587,7 @@ class ExamViews(View, LoginRequiredMixin): raise Http404("No available exam") # TODO: check access for logged in users + print("TEST", cid) # Check access for users if request.user.is_anonymous: @@ -1722,9 +1723,12 @@ class ExamViews(View, LoginRequiredMixin): return self.exam_question_json_unbased(request, pk, sk, cid, passcode) def exam_question_json_unbased(self, request, pk, sk, cid=None, passcode=None): + print("TESTING") + print("**************************") question = get_object_or_404(self.Question, pk=sk) exam = get_object_or_404(self.Exam, pk=pk) + print("CID", cid) if not exam.active and not self.check_user_access(request.user, pk): raise Http404("No available exam") @@ -1735,6 +1739,8 @@ class ExamViews(View, LoginRequiredMixin): else: user_id = request.user.pk + print("CID", cid) + if not exam.check_cid_user(cid, passcode, request, user_id): raise Http404("No available exam") @@ -1795,16 +1801,19 @@ class ExamViews(View, LoginRequiredMixin): ).first() # user_answer = cid_user_answers_q_map[q] + print("QUSETION", q) if not user_answer or user_answer is None: # skip if no answer score, text = q.get_unanswered_mark_and_text() - answers_marks.append(score) + #answers_marks.append(score) # answers.append("") answer_score = score ans = text else: ans = user_answer.get_answer() answer_score = user_answer.get_answer_score() + print("ans", ans) + print("ans score", answer_score) correct_answer = q.get_primary_answer() @@ -1815,6 +1824,8 @@ class ExamViews(View, LoginRequiredMixin): answers_marks.append(answer_score) answers_and_marks.append((ans, answer_score, correct_answer)) + print(answers_marks) + if "unmarked" in answers_marks: answered = [i for i in answers_marks if type(i) == int] total_score = sum(answered) @@ -1823,7 +1834,14 @@ class ExamViews(View, LoginRequiredMixin): else: total_score = sum(answers_marks) - max_score = len(questions) * 2 + match self.app_name: + case "rapids" | "anatomy": + max_score = len(questions) * 2 + case "longs": + max_score = len(questions) * 8 + case _: + max_score = len(questions) + template_context = { "exam": exam, diff --git a/longs/models.py b/longs/models.py index 18809b7e..f7daac8f 100644 --- a/longs/models.py +++ b/longs/models.py @@ -345,9 +345,10 @@ class Long(QuestionBase): return url - def get_unanswered_mark_and_text(self) -> tuple[int, str]: + def get_unanswered_mark_and_text(self) -> tuple[int, tuple[str, str, str, str, str]]: """ - Override in models if needed + Long cases are receive a mark between 4 and 8 + Therefore unmarked = 4 """ return (4, ("Not answered",) * 5) @@ -862,7 +863,9 @@ class UserAnswer(UserAnswerBase): return "hidden" return "/".join(f"{str(i.score)}" for i in mark_objects) - def get_answer_score(self): + def get_answer_score(self) -> float | str: + """Returns the answers score. + If the answer is unmarked the string "unmarked" is returned""" if self.score == "": return "unmarked" return float(self.score) @@ -873,8 +876,19 @@ class UserAnswer(UserAnswerBase): return True return False - def get_answer(self): - ( + def get_answer( + self, + ) -> tuple[ + models.TextField, + models.TextField, + models.TextField, + models.TextField, + models.TextField, + ]: + """Returns a tuple containing the users answers (model fields) + + """ + return ( self.answer_observations, self.answer_interpretation, self.answer_principle_diagnosis, @@ -901,7 +915,6 @@ Management: """ - @reversion.register class AnswerMarks(models.Model): score = models.CharField(max_length=3, choices=UserAnswer.ScoreOptions.choices) diff --git a/longs/templates/longs/exam_scores_user.html b/longs/templates/longs/exam_scores_user.html index c7c40a59..0a393643 100644 --- a/longs/templates/longs/exam_scores_user.html +++ b/longs/templates/longs/exam_scores_user.html @@ -13,38 +13,34 @@
{% include 'user_score_header.html' %} -
    - {% for score in answers_marks %} -
  • Question {{forloop.counter}}
  • - -
    {{ans}}
    ({{score}}) -
    - {% endfor %} -
-

Answers

-
    {% for a,b,c,d,e in answer_text %} -
  • Question {{forloop.counter}} View
  • - -
      -
    • Observation
      -
      {{a}}
      + {% comment %}

      Answers

      {% endcomment %} +
        + {% for ans, score, correct_answer in answers_and_marks %} +
      • Question {{forloop.counter}} View +
          +
        • Observations
          +
          {{ans.0}}
          +
        • +
        • Interpretation
          +
          {{ans.1}}
          +
        • +
        • Principle Diagnosis
          +
          {{ans.2}}
          +
        • +
        • Differential Diagnosis
          +
          {{ans.3}}
          +
        • +
        • Management
          +
          {{ans.4}}
          +
        • + {% if exam.publish_results %} + {{score}} + {% endif %} +
      • -
      • Interpretation
        -
        {{b}}
        -
      • -
      • Principle Diagnosis
        -
        {{c}}
        -
      • -
      • Differential Diagnosis
        -
        {{d}}
        -
      • -
      • Management
        -
        {{e}}
        -
      • -
      - {% endfor %} + {% endfor %}
@@ -73,42 +69,42 @@ }) // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly .done(function (data) { - console.log(data); + console.log(data); - let event = new CustomEvent('loadDicomViewerUrls', { - "detail": { - "images": data.images[0], - "annotations": data.annotations - } - }); + let event = new CustomEvent('loadDicomViewerUrls', { + "detail": { + "images": data.images[0], + "annotations": data.annotations + } + }); - window.dispatchEvent(event); - n = parseInt(question_number) + 1 - $(".question-display-block .question-number").empty().append(n); - $("#history").empty().append(data.title); - $("#series").empty(); - data.image_titles.forEach((element, index) => { - $("#series").append($( - `${index + 1}: ${element} ` - ).click(() => { - $("#single-dicom-viewer").empty(); - let event = new CustomEvent('loadDicomViewerUrls', { - "detail": { - "images": data.images[index], - "annotations": data.annotations - } - }); + window.dispatchEvent(event); + n = parseInt(question_number) + 1 + $(".question-display-block .question-number").empty().append(n); + $("#history").empty().append(data.title); + $("#series").empty(); + data.image_titles.forEach((element, index) => { + $("#series").append($( + `${index + 1}: ${element} ` + ).click(() => { + $("#single-dicom-viewer").empty(); + let event = new CustomEvent('loadDicomViewerUrls', { + "detail": { + "images": data.images[index], + "annotations": data.annotations + } + }); - window.dispatchEvent(event); + window.dispatchEvent(event); - })); + })); + }) + $(".inner-display-block").show(); }) - $(".inner-display-block").show(); - }) .always(function () { - console.log('[Done]'); - }) + console.log('[Done]'); + }) }) $(".view-question-link").first().click(); diff --git a/longs/tests/test_longs_exams.py b/longs/tests/test_longs_exams.py index 8899d211..0c58f420 100644 --- a/longs/tests/test_longs_exams.py +++ b/longs/tests/test_longs_exams.py @@ -1,3 +1,4 @@ +from collections import defaultdict import json import os from re import A @@ -23,6 +24,7 @@ from longs.models import ( Long as Question, LongSeries, LongSeriesImage, + ) from generic.models import CidUser, CidUserGroup, UserUserGroup @@ -31,6 +33,8 @@ from django.contrib.auth.models import User from rad.test_helpers import AssertNotFound, create_cid_user_and_groups + + APP_NAME = "longs" @@ -223,18 +227,30 @@ def test_exams(db, client): print("check question:", q) # check redirects to the json file work # TODO: sort out testing of the saved JSON files - 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, - ) + 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 @@ -242,18 +258,42 @@ def test_exams(db, client): assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json") - 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, - ) + print(cid_user, testing_cid_user) + + 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, + }, + ) + print(test) + 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) @@ -262,9 +302,7 @@ def test_exams(db, client): # 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 - ) + q_json = Question.objects.get(pk=q).get_question_json(based=False) assert question_json == q_json @@ -325,17 +363,20 @@ def test_exams(db, client): # 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 - sections = ( - ("1", "answer observations"), - ("2", "answer interpretation"), - ("3", "answer principle diagnosis"), - ("4", "answer differential diagnosis"), - ("5", "answer management"), - ) for n, name in sections: post_data = { "eid": exam_json["eid"], @@ -348,6 +389,8 @@ def test_exams(db, client): 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"], @@ -469,21 +512,32 @@ def test_exams(db, client): answer_lis = cid_exam_scores_soup.find( "ul", {"class": "score-answer-list"} - ).find_all("li") + ).find_all("li", {"class": "user-answer-li"}) - assert len(answer_lis) == len(valid_answers) + assert len(answer_lis) == len(valid_answers_by_question) + + pprint(valid_answers_by_question) + print(cid_user) for n in range( - len(valid_answers) + len(valid_answers_by_question) ): # Check no answers are visible if results not published - answer = valid_answers[n] - li = answer_lis[n] + lis = answer_lis[n] + #n = str(n + 1) # questions are not zero indexed (lists are) - assert answer["ans"] in str(li) # Check that the saved answer is shown + for qidn, section_title in sections: + print(n, qidn) + answer = valid_answers_by_question[n][qidn] - assert not li.find( - "span", {"class": "correct-answer"} - ) # and that the correct answer isn't + 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 @@ -498,9 +552,10 @@ def test_exams(db, client): answer_lis = cid_exam_scores_soup.find( "ul", {"class": "score-answer-list"} - ).find_all("li") - assert len(answer_lis) == len(valid_answers) + 1 + ).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 @@ -516,51 +571,62 @@ def test_exams(db, client): answer_lis = cid_exam_scores_soup.find( "ul", {"class": "score-answer-list"} - ).find_all("li") + ).find_all("li", {"class": "user-answer-li"}) - assert len(answer_lis) == len(valid_answers) + 1 + 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) - ): # Check answers are visible if results published - answer = valid_answers[n] - li = answer_lis[n] + 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) - assert answer["ans"] in str(li) # Check that the saved answer is shown + for qidn, section_title in sections: + answer = valid_answers_by_question[n][qidn] - assert li.find("span", {"class": "correct-answer"}).string == "testanswer" + li = lis.find("li", attrs={"data-qidn": qidn}) - 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)" - ) + assert li["class"][0].lower() in section_title + assert answer["ans"] in str(li) # Check that the saved answer is shown + + print(cid_exam_scores_soup.find("div", {"class": "score-overview"})) + + # 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 + == "4 (3 unmarked)" + ) + + # Mark the questions + # For longs this needs to be done for each user + question: Question 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.save() - except IndexError: # Final question does not have an answer - pass + + answers = question.get_unmarked_user_answers() + print(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) @@ -570,7 +636,7 @@ def test_exams(db, client): cid_exam_scores_soup.find("div", {"class": "score-overview"}) .find("span", {"id": "total-score"}) .string - == "6" + == "28.0" ) generated_questions.pop(-1).delete() diff --git a/templates/user_score_header.html b/templates/user_score_header.html index bbbd54f5..c52556ae 100644 --- a/templates/user_score_header.html +++ b/templates/user_score_header.html @@ -1,7 +1,20 @@ {% if view_all_results %} {% else %} {% if not exam.publish_results %}