From ae670c738b78dcb824e3d6e3031e5e6fe4612c8e Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 22 May 2023 13:29:07 +0100 Subject: [PATCH] more Long improvements --- TODO.md | 1 + generic/models.py | 6 ++ generic/views.py | 11 ++-- longs/models.py | 29 --------- longs/tables.py | 74 ++++++++++++++++++----- longs/tests/test_longs_exams.py | 97 +++++++++++++++++++------------ longs/views.py | 1 + rapids/tests/test_rapids_exams.py | 4 +- 8 files changed, 136 insertions(+), 87 deletions(-) diff --git a/TODO.md b/TODO.md index f451b742..0b08a2ca 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ # Current target - User management +- SQUASH MIGRATIONS # Ongoing diff --git a/generic/models.py b/generic/models.py index 1cf4611d..bc81e96c 100644 --- a/generic/models.py +++ b/generic/models.py @@ -74,6 +74,12 @@ class QuestionBase(models.Model): class Meta: abstract = True + def get_unanswered_mark_and_text(self) -> tuple(int, str): + """ + Override in models if needed + """ + return (0, "Not answered") + class ExamBase(models.Model): name = models.CharField(max_length=200, help_text="Name of the exam") diff --git a/generic/views.py b/generic/views.py index 962783cc..90414fd0 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1687,7 +1687,7 @@ class ExamViews(View, LoginRequiredMixin): else: cid_user = None exam.exam_user_status.create( - cid_user=cid, user_user=user, status="downloaded", extra="unbased" + cid_user=cid_user, user_user=user, status="downloaded", extra="unbased" ) return JsonResponse(exam_json) @@ -1725,6 +1725,7 @@ class ExamViews(View, LoginRequiredMixin): question = get_object_or_404(self.Question, pk=sk) exam = get_object_or_404(self.Exam, pk=pk) + if not exam.active and not self.check_user_access(request.user, pk): raise Http404("No available exam") @@ -1733,6 +1734,7 @@ class ExamViews(View, LoginRequiredMixin): user_id = None else: user_id = request.user.pk + if not exam.check_cid_user(cid, passcode, request, user_id): raise Http404("No available exam") @@ -1795,10 +1797,11 @@ class ExamViews(View, LoginRequiredMixin): if not user_answer or user_answer is None: # skip if no answer - answers_marks.append(0) + score, text = q.get_unanswered_mark_and_text() + answers_marks.append(score) # answers.append("") - answer_score = 0 - ans = "Not answered" + answer_score = score + ans = text else: if exam.app_name == "rapids" and user_answer.normal: ans = "Normal" diff --git a/longs/models.py b/longs/models.py index dee8bb37..555a43b1 100644 --- a/longs/models.py +++ b/longs/models.py @@ -655,35 +655,6 @@ class Exam(ExamBase): exam_user_status = GenericRelation(ExamUserStatus) - def get_exam_question_json(self, question_id): - q = get_object_or_404(Long, pk=question_id) - - # exam_order.append(q.id) - - # Loop through longimage associations - images = [] - image_titles = [] - for series in q.series.all(): - # image_array = [] - # for i in series.images.all(): - # image_array.append(image_as_base64(i.image)) - # #image_array.append(i.image.url) - image_array = [image_as_base64(i.image) for i in series.images.all()] - images.append(image_array) - image_titles.append(series.get_examination()) - - exam_question = { - "title": q.history, - "images": images, - "image_titles": image_titles, - # "feedback_image": [], - # "annotations": [str(q.image_annotations)], - "type": "long", - "images_json": True, - } - - return exam_question - def get_exam_json(self, based=True): questions = self.exam_questions.all() diff --git a/longs/tables.py b/longs/tables.py index df8c0d95..1aaca779 100755 --- a/longs/tables.py +++ b/longs/tables.py @@ -4,6 +4,7 @@ from django_tables2.utils import A from .models import Long, LongSeries, UserAnswer from django.utils.html import format_html +from django.db.models import Prefetch from easy_thumbnails.files import get_thumbnailer @@ -12,10 +13,12 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class LongImageColumn(tables.Column): def render(self, value): - blocks =[] + blocks = [] for obj in value.all(): blocks.append(obj.get_block()) - return format_html("{}".format("".join(blocks))) + return format_html( + "{}".format("".join(blocks)) + ) obj = value.first() @@ -42,9 +45,17 @@ class LongSeriesImageColumn(tables.Column): image_object = obj[0].image try: thumbnailer = get_thumbnailer(image_object) - return format_html('
[{}]', thumbnailer["exam-list"], value.count()) + return format_html( + '
[{}]', + thumbnailer["exam-list"], + value.count(), + ) except InvalidImageFormatError: - return format_html('Invalid image url
[{}]', image_object, value.count()) + return format_html( + 'Invalid image url
[{}]', + image_object, + value.count(), + ) class LongTable(tables.Table): @@ -62,7 +73,7 @@ class LongTable(tables.Table): ) series = LongImageColumn("Images", orderable=False) - #series = tables.ManyToManyColumn(verbose_name="Exams") + # series = tables.ManyToManyColumn(verbose_name="Exams") exams = tables.ManyToManyColumn(verbose_name="Exams") selection = tables.CheckBoxColumn(accessor="pk", orderable=False) @@ -76,12 +87,19 @@ class LongTable(tables.Table): def __init__(self, data=None, *args, **kwargs): super().__init__( data.prefetch_related( - "series", "author", "exams", "series__images", "series__examination", "series__plane", "series__contrast" + "series", + "author", + "exams", + "series__images", + "series__examination", + "series__plane", + "series__contrast", ), *args, **kwargs, ) + class PopupLinkColumn(tables.Column): def render(self, value): return format_html("test: {}", value) @@ -120,7 +138,14 @@ class LongSeriesTable(tables.Table): def __init__(self, data=None, *args, **kwargs): super().__init__( data.prefetch_related( - "long", "long__series", "images", "author", "examination", "plane", "contrast", "modality" + "long", + "long__series", + "images", + "author", + "examination", + "plane", + "contrast", + "modality", ), *args, **kwargs, @@ -128,17 +153,21 @@ class LongSeriesTable(tables.Table): def render_popup(self, value, record): print(self) - return format_html("""Popup""", record.pk) + return format_html( + """Popup""", + record.pk, + ) + class UserAnswerTable(tables.Table): select = tables.CheckBoxColumn(accessor=("pk")) delete = tables.LinkColumn( "longs:user_answer_delete", text="Delete", args=[A("pk")], orderable=False ) - view = tables.LinkColumn('longs:user_answer_view', - text='View', - args=[A('pk')], - orderable=False) + view = tables.LinkColumn( + "longs:user_answer_view", text="View", args=[A("pk")], orderable=False + ) + class Meta: model = UserAnswer template_name = "django_tables2/bootstrap4.html" @@ -146,11 +175,24 @@ class UserAnswerTable(tables.Table): "cid", "question", "score", - #"normal", - #"answer", - #"answer_compare", + # "normal", + # "answer", + # "answer_compare", "exam", "created", "updated", ) - \ No newline at end of file + + def __init__(self, data=None, *args, **kwargs): + super().__init__( + data.prefetch_related( + Prefetch( + "question", # "question__exams"#, "author", "exams", "series__images", "series__examination", "series__plane", "series__contrast" + #queryset=Long.objects.select_related().prefetch_related( + # Prefetch("exams") + #), + ) + ), + *args, + **kwargs, + ) diff --git a/longs/tests/test_longs_exams.py b/longs/tests/test_longs_exams.py index 553b5335..8899d211 100644 --- a/longs/tests/test_longs_exams.py +++ b/longs/tests/test_longs_exams.py @@ -203,7 +203,21 @@ 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) + + # 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"]: print("check question:", q) @@ -212,37 +226,48 @@ def test_exams(db, client): 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 + kwargs={ + "pk": exam.pk, + "sk": q, + "cid": cid_user.cid, + "passcode": cid_user.passcode, + }, + ), + follow=True, ) - #assert question_json_res.status_code == 200 - print(question_json_res.redirect_chain) - print(question_json_res.content) + # 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") - 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 + kwargs={ + "pk": exam.pk, + "sk": q, + "cid": cid_user.cid, + "passcode": cid_user.passcode, + }, + ), + 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)) - #print(question_json) + # question_json_res = client.get(question1.get_question_json(answers=False, based=True)) - assert ( - question_json - in exam_json["questions"].values() + # 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 @@ -303,26 +328,26 @@ def test_exams(db, client): for questions in exam_json["questions"]: for qid in questions: - 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 + # 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"], + "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.append(post_data2) + valid_answers_extra.append(post_data) post_json = { "eid": exam_json["eid"], @@ -337,8 +362,8 @@ def test_exams(db, client): json_res = json.loads(res.content) - assert json_res["success"] == True - assert json_res["question_count"] == 6 + 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: diff --git a/longs/views.py b/longs/views.py index a4868864..af014048 100755 --- a/longs/views.py +++ b/longs/views.py @@ -1153,6 +1153,7 @@ def question_json_unbased(request, pk): """ No (file based) caching is enabled for unbased quesitons """ + print("UNBASED") question = get_object_or_404(Long, pk=pk) question_json = question.get_question_json(based=False) diff --git a/rapids/tests/test_rapids_exams.py b/rapids/tests/test_rapids_exams.py index b334bf7a..06203a0c 100644 --- a/rapids/tests/test_rapids_exams.py +++ b/rapids/tests/test_rapids_exams.py @@ -290,8 +290,8 @@ def test_exams(db, client): json_res = json.loads(res.content) - assert json_res["success"] == True - assert json_res["question_count"] == 6 + assert json_res["success"] is True + assert json_res["question_count"] == 2 * 3 # Check the answers now exist in the database if testing_cid_user: