fix longs tests

This commit is contained in:
Ross
2023-06-05 16:44:35 +01:00
parent e0723d4d3a
commit afdc8e95c7
6 changed files with 278 additions and 152 deletions
+24 -4
View File
@@ -286,25 +286,45 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
), ),
# These are a little more ambiguous than ideal # These are a little more ambiguous than ideal
path( path(
"exam/json/<int:pk>/<int:sk>", "exam/json/<int:pk>/question/<int:sk>",
generic_exam_view.exam_question_json, generic_exam_view.exam_question_json,
name="exam_question_json", name="exam_question_json",
), ),
path( path(
"exam/json/<int:pk>/<int:sk>/unbased", "exam/json/<int:pk>/question/<int:sk>/unbased",
generic_exam_view.exam_question_json_unbased, generic_exam_view.exam_question_json_unbased,
name="exam_question_json_unbased", name="exam_question_json_unbased",
), ),
path( path(
"exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>", "exam/json/<int:pk>/question/<int:cid>/<str:passcode>/<int:sk>",
generic_exam_view.exam_question_json_cid, generic_exam_view.exam_question_json_cid,
name="exam_question_json_cid", name="exam_question_json_cid",
), ),
path( path(
"exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>/unbased", "exam/json/<int:pk>/question/<int:cid>/<str:passcode>/<int:sk>/unbased",
generic_exam_view.exam_question_json_unbased_cid, generic_exam_view.exam_question_json_unbased_cid,
name="exam_question_json_unbased_cid", name="exam_question_json_unbased_cid",
), ),
#path(
# "exam/json/<int:pk>/<int:sk>",
# generic_exam_view.exam_question_json,
# name="exam_question_json",
#),
#path(
# "exam/json/<int:pk>/<int:sk>/unbased",
# generic_exam_view.exam_question_json_unbased,
# name="exam_question_json_unbased",
#),
#path(
# "exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>",
# generic_exam_view.exam_question_json_cid,
# name="exam_question_json_cid",
#),
#path(
# "exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>/unbased",
# generic_exam_view.exam_question_json_unbased_cid,
# name="exam_question_json_unbased_cid",
#),
path( path(
"exam/json/<int:pk>/recreate", "exam/json/<int:pk>/recreate",
generic_exam_view.exam_json_recreate, generic_exam_view.exam_json_recreate,
+19 -1
View File
@@ -1587,6 +1587,7 @@ class ExamViews(View, LoginRequiredMixin):
raise Http404("No available exam") raise Http404("No available exam")
# TODO: check access for logged in users # TODO: check access for logged in users
print("TEST", cid)
# Check access for users # Check access for users
if request.user.is_anonymous: if request.user.is_anonymous:
@@ -1722,9 +1723,12 @@ class ExamViews(View, LoginRequiredMixin):
return self.exam_question_json_unbased(request, pk, sk, cid, passcode) return self.exam_question_json_unbased(request, pk, sk, cid, passcode)
def exam_question_json_unbased(self, request, pk, sk, cid=None, passcode=None): 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) question = get_object_or_404(self.Question, pk=sk)
exam = get_object_or_404(self.Exam, pk=pk) 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): if not exam.active and not self.check_user_access(request.user, pk):
raise Http404("No available exam") raise Http404("No available exam")
@@ -1735,6 +1739,8 @@ class ExamViews(View, LoginRequiredMixin):
else: else:
user_id = request.user.pk user_id = request.user.pk
print("CID", cid)
if not exam.check_cid_user(cid, passcode, request, user_id): if not exam.check_cid_user(cid, passcode, request, user_id):
raise Http404("No available exam") raise Http404("No available exam")
@@ -1795,16 +1801,19 @@ class ExamViews(View, LoginRequiredMixin):
).first() ).first()
# user_answer = cid_user_answers_q_map[q] # user_answer = cid_user_answers_q_map[q]
print("QUSETION", q)
if not user_answer or user_answer is None: if not user_answer or user_answer is None:
# skip if no answer # skip if no answer
score, text = q.get_unanswered_mark_and_text() score, text = q.get_unanswered_mark_and_text()
answers_marks.append(score) #answers_marks.append(score)
# answers.append("") # answers.append("")
answer_score = score answer_score = score
ans = text ans = text
else: else:
ans = user_answer.get_answer() ans = user_answer.get_answer()
answer_score = user_answer.get_answer_score() answer_score = user_answer.get_answer_score()
print("ans", ans)
print("ans score", answer_score)
correct_answer = q.get_primary_answer() correct_answer = q.get_primary_answer()
@@ -1815,6 +1824,8 @@ class ExamViews(View, LoginRequiredMixin):
answers_marks.append(answer_score) answers_marks.append(answer_score)
answers_and_marks.append((ans, answer_score, correct_answer)) answers_and_marks.append((ans, answer_score, correct_answer))
print(answers_marks)
if "unmarked" in answers_marks: if "unmarked" in answers_marks:
answered = [i for i in answers_marks if type(i) == int] answered = [i for i in answers_marks if type(i) == int]
total_score = sum(answered) total_score = sum(answered)
@@ -1823,7 +1834,14 @@ class ExamViews(View, LoginRequiredMixin):
else: else:
total_score = sum(answers_marks) total_score = sum(answers_marks)
match self.app_name:
case "rapids" | "anatomy":
max_score = len(questions) * 2 max_score = len(questions) * 2
case "longs":
max_score = len(questions) * 8
case _:
max_score = len(questions)
template_context = { template_context = {
"exam": exam, "exam": exam,
+19 -6
View File
@@ -345,9 +345,10 @@ class Long(QuestionBase):
return url 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) return (4, ("Not answered",) * 5)
@@ -862,7 +863,9 @@ class UserAnswer(UserAnswerBase):
return "hidden" return "hidden"
return "/".join(f"{str(i.score)}" for i in mark_objects) 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 == "": if self.score == "":
return "unmarked" return "unmarked"
return float(self.score) return float(self.score)
@@ -873,8 +876,19 @@ class UserAnswer(UserAnswerBase):
return True return True
return False 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_observations,
self.answer_interpretation, self.answer_interpretation,
self.answer_principle_diagnosis, self.answer_principle_diagnosis,
@@ -901,7 +915,6 @@ Management:
""" """
@reversion.register @reversion.register
class AnswerMarks(models.Model): class AnswerMarks(models.Model):
score = models.CharField(max_length=3, choices=UserAnswer.ScoreOptions.choices) score = models.CharField(max_length=3, choices=UserAnswer.ScoreOptions.choices)
+19 -23
View File
@@ -13,37 +13,33 @@
</div> </div>
<div class="longs"> <div class="longs">
{% include 'user_score_header.html' %} {% include 'user_score_header.html' %}
<ul>
{% for score in answers_marks %}
<li class="user-answer-li">Question {{forloop.counter}}</li>
<span class="user-answer-score user-answer-score-{{score}}">
<pre>{{ans}}</pre> ({{score}})
</span>
{% endfor %}
</ul>
<div> <div>
<h4>Answers</h4> {% comment %} <h4>Answers</h4> {% endcomment %}
<ul class="long-answer">{% for a,b,c,d,e in answer_text %} <ul class="score-answer-list">
<li class="user-answer-li"><b>Question {{forloop.counter}}</b> <span class="view-question-link-longs" {% for ans, score, correct_answer in answers_and_marks %}
data-qn={{forloop.counter0}}>View</span></li> <li class="user-answer-li" data-question-number="{{forloop.counter}}"><b>Question {{forloop.counter}}</b> <span class="view-question-link-longs"
data-qn={{forloop.counter0}}>View</span>
<ul> <ul>
<li class="user-answer-li">Observation</br> <li class="Observations" data-qidn="1">Observations</br>
<pre>{{a}}</pre> <pre>{{ans.0}}</pre>
</li> </li>
<li class="user-answer-li">Interpretation</br> <li class="Interpretation" data-qidn="2">Interpretation</br>
<pre>{{b}}</pre> <pre>{{ans.1}}</pre>
</li> </li>
<li class="user-answer-li">Principle Diagnosis</br> <li class="Principle Diagnosis" data-qidn="3">Principle Diagnosis</br>
<pre>{{c}}</pre> <pre>{{ans.2}}</pre>
</li> </li>
<li class="user-answer-li">Differential Diagnosis</br> <li class="Differential Diagnosis" data-qidn="4">Differential Diagnosis</br>
<pre>{{d}}</pre> <pre>{{ans.3}}</pre>
</li> </li>
<li class="user-answer-li">Management</br> <li class="Management" data-qidn="5">Management</br>
<pre>{{e}}</pre> <pre>{{ans.4}}</pre>
</li> </li>
{% if exam.publish_results %}
<span class="answer-score">{{score}}</span>
{% endif %}
</ul> </ul>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
+111 -45
View File
@@ -1,3 +1,4 @@
from collections import defaultdict
import json import json
import os import os
from re import A from re import A
@@ -23,6 +24,7 @@ from longs.models import (
Long as Question, Long as Question,
LongSeries, LongSeries,
LongSeriesImage, LongSeriesImage,
) )
from generic.models import CidUser, CidUserGroup, UserUserGroup 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 from rad.test_helpers import AssertNotFound, create_cid_user_and_groups
APP_NAME = "longs" APP_NAME = "longs"
@@ -223,6 +227,7 @@ def test_exams(db, client):
print("check question:", q) print("check question:", q)
# check redirects to the json file work # check redirects to the json file work
# TODO: sort out testing of the saved JSON files # TODO: sort out testing of the saved JSON files
try:
question_json_res = client.get( question_json_res = client.get(
reverse( reverse(
f"longs:exam_question_json_cid", f"longs:exam_question_json_cid",
@@ -235,6 +240,17 @@ def test_exams(db, client):
), ),
follow=True, 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 question_json_res.status_code == 200
assert len(question_json_res.redirect_chain) == 2 assert len(question_json_res.redirect_chain) == 2
assert question_json_res.redirect_chain[0][1] == 302 assert question_json_res.redirect_chain[0][1] == 302
@@ -242,6 +258,9 @@ def test_exams(db, client):
assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json") assert question_json_res.redirect_chain[1][0].endswith(f"{q}.json")
print(cid_user, testing_cid_user)
if testing_cid_user:
question_json_unbased_res = client.get( question_json_unbased_res = client.get(
reverse( reverse(
f"longs:exam_question_json_unbased_cid", f"longs:exam_question_json_unbased_cid",
@@ -254,6 +273,27 @@ def test_exams(db, client):
), ),
follow=True, 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 assert question_json_unbased_res.status_code == 200
question_json = json.loads(question_json_unbased_res.content) 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 # As we store references to the questions we have to check individually
assert q in exam_json["questions"].keys() assert q in exam_json["questions"].keys()
q_json = Question.objects.get(pk=q).get_question_json( q_json = Question.objects.get(pk=q).get_question_json(based=False)
based=False
)
assert question_json == q_json assert question_json == q_json
@@ -325,10 +363,8 @@ def test_exams(db, client):
# Long answers currently send a seperate object for each component # Long answers currently send a seperate object for each component
valid_answers = [] valid_answers = []
valid_answers_extra = [] valid_answers_extra = []
valid_answers_by_question = defaultdict(dict)
for questions in exam_json["questions"]:
for qid in questions:
# Long answers submit 5 sections
sections = ( sections = (
("1", "answer observations"), ("1", "answer observations"),
("2", "answer interpretation"), ("2", "answer interpretation"),
@@ -336,6 +372,11 @@ def test_exams(db, client):
("4", "answer differential diagnosis"), ("4", "answer differential diagnosis"),
("5", "answer management"), ("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: for n, name in sections:
post_data = { post_data = {
"eid": exam_json["eid"], "eid": exam_json["eid"],
@@ -348,6 +389,8 @@ def test_exams(db, client):
post_data["passcode"] = cid_user.passcode post_data["passcode"] = cid_user.passcode
valid_answers_extra.append(post_data) valid_answers_extra.append(post_data)
valid_answers_by_question[i][n] = post_data
i = i + 1
post_json = { post_json = {
"eid": exam_json["eid"], "eid": exam_json["eid"],
@@ -469,21 +512,32 @@ def test_exams(db, client):
answer_lis = cid_exam_scores_soup.find( answer_lis = cid_exam_scores_soup.find(
"ul", {"class": "score-answer-list"} "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( for n in range(
len(valid_answers) len(valid_answers_by_question)
): # Check no answers are visible if results not published ): # Check no answers are visible if results not published
answer = valid_answers[n] lis = answer_lis[n]
li = answer_lis[n] #n = str(n + 1) # questions are not zero indexed (lists are)
for qidn, section_title in sections:
print(n, qidn)
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 answer["ans"] in str(li) # Check that the saved answer is shown
assert not li.find( # assert not li.find(
"span", {"class": "correct-answer"} # "span", {"class": "correct-answer"}
) # and that the correct answer isn't # ) # and that the correct answer isn't
assert ( assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"}) is None 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( answer_lis = cid_exam_scores_soup.find(
"ul", {"class": "score-answer-list"} "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 "Not answered" in str(answer_lis[-1])
assert str(answer_lis[-1]).count("Not answered") == 5
# Publish the results! # Publish the results!
exam.publish_results = True exam.publish_results = True
@@ -516,51 +571,62 @@ def test_exams(db, client):
answer_lis = cid_exam_scores_soup.find( answer_lis = cid_exam_scores_soup.find(
"ul", {"class": "score-answer-list"} "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 "Not answered" in str(answer_lis[-1])
assert str(answer_lis[-1]).count("Not answered") == 5
# Repetition.... # Repetition....
for n in range( for n in range(
len(valid_answers) len(valid_answers_by_question)
): # Check answers are visible if results published ): # Check no answers are visible if results not published
answer = valid_answers[n] lis = answer_lis[n]
li = 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 answer["ans"] in str(li) # Check that the saved answer is shown
assert li.find("span", {"class": "correct-answer"}).string == "testanswer" print(cid_exam_scores_soup.find("div", {"class": "score-overview"}))
if users_tested == 1: # For long cases an unmarked answers is given a score of 4
# First user the answers will not have been saved # Each user needs marking individually
assert ( assert (
cid_exam_scores_soup.find("div", {"class": "score-overview"}) cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"}) .find("span", {"id": "total-score"})
.string .string
== "0 (3 unmarked)" == "4 (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)"
) )
# Mark the questions
# For longs this needs to be done for each user
question: Question
for question in exam.exam_questions.all(): for question in exam.exam_questions.all():
print(question) print(question)
print(question.get_compare_answers())
try: answers = question.get_unmarked_user_answers()
new_ans = Answer( print(answers)
question=question, for answer in answers:
answer=question.get_unmarked_user_answers().pop(), answer.score = UserAnswer.ScoreOptions.EIGHT
status=Answer.MarkOptions.CORRECT, answer.save()
)
new_ans.save() #try:
except IndexError: # Final question does not have an answer # new_ans = Answer(
pass # 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) # Load the exam results page (again)
cid_exam_scores_res = client.get(exam_scores_url) 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"}) cid_exam_scores_soup.find("div", {"class": "score-overview"})
.find("span", {"id": "total-score"}) .find("span", {"id": "total-score"})
.string .string
== "6" == "28.0"
) )
generated_questions.pop(-1).delete() generated_questions.pop(-1).delete()
+14 -1
View File
@@ -1,7 +1,20 @@
{% if view_all_results %} {% if view_all_results %}
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
Exam state: Active [{{exam.active}}] / Published [{{exam.publish_results}}] Exam state:
{% if exam.active %}
This exam is available to take.
{% else %}
This exam is NOT available to take.
{% endif %}
{% if exam.publish_results %}
Results are published.
{% else %}
Results are NOT published.
{% endif %}
</div> </div>
{% else %} {% else %}
{% if not exam.publish_results %} {% if not exam.publish_results %}