From 6b5c3f891460e6cae0b282be734624343661a8d0 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 3 Jul 2023 11:10:50 +0100 Subject: [PATCH] allow merging of examinations --- atlas/models.py | 8 ++- atlas/templates/atlas/categories_list.html | 5 ++ generic/forms.py | 2 + generic/models.py | 57 ++++++++++++++- .../templates/generic/examination_detail.html | 37 +++++++--- .../templates/generic/examination_merge.html | 14 ++++ generic/urls.py | 1 + generic/views.py | 72 ++++++++++++++----- longs/models.py | 3 + longs/tables.py | 2 +- longs/templates/longs/long_display_block.html | 2 +- longs/templates/longs/mark_answer.html | 2 +- longs/urls.py | 2 +- longs/views.py | 10 +-- 14 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 generic/templates/generic/examination_merge.html diff --git a/atlas/models.py b/atlas/models.py index 84689252..191b72f4 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -304,10 +304,11 @@ class Case(models.Model): return authors def get_link(self): + return f"{self.pk}: {self.title}" return format_html("{}", self.get_absolute_url(), str(self)) def __str__(self): - return f"{self.pk}:{self.title}" + return f"{self.pk}: {self.title}" def get_series_blocks(self): html = "" @@ -391,7 +392,7 @@ class Series(SeriesBase): def get_full_str(self): if self.case: - case_id = ", ".join([case.pk for case in self.case.all()]) + case_id = ", ".join([str(case) for case in self.case.all()]) # case_id = self.case.pk else: case_id = "None" @@ -403,6 +404,9 @@ class Series(SeriesBase): def get_absolute_url(self): return reverse("atlas:series_detail", kwargs={"pk": self.pk}) + def get_link(self): + return format_html("{}", self.get_absolute_url(), self.get_full_str()) + class CaseCollection(ExamCollectionGenericBase): diff --git a/atlas/templates/atlas/categories_list.html b/atlas/templates/atlas/categories_list.html index 5fb91059..6d61699e 100644 --- a/atlas/templates/atlas/categories_list.html +++ b/atlas/templates/atlas/categories_list.html @@ -9,4 +9,9 @@
  • Presentations
  • Pathological Process
  • + + {% if request.user.is_staff %} +

    Manage Examinations +

    + {% endif %} {% endblock %} diff --git a/generic/forms.py b/generic/forms.py index 2ce58f88..b59bbdcd 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -128,6 +128,8 @@ class ExaminationForm(ModelForm): model = Examination fields = ["examination", "modality"] +class ExaminationMergeForm(Form): + examination_to_merge_into = ModelChoiceField(queryset=Examination.objects.all()) class QuestionNoteForm(ModelForm): class Meta: diff --git a/generic/models.py b/generic/models.py index 77d6cdda..1b8148c4 100644 --- a/generic/models.py +++ b/generic/models.py @@ -1,7 +1,7 @@ from collections import defaultdict import json import os -from typing import Tuple +from typing import Self, Tuple from django.contrib.auth.mixins import LoginRequiredMixin from django.db import models from django.http import HttpRequest @@ -72,15 +72,66 @@ class Contrast(models.Model): class Examination(models.Model): + """ + + Currently used for + - Atlas series + - Longs series + - Anatomy questions + + Not used for rapids - yet?? + """ + examination = models.CharField(max_length=200, unique=True) modality = models.ForeignKey(Modality, on_delete=models.SET_NULL, null=True) + class Meta: + ordering = ("examination",) + def __str__(self): return self.examination - class Meta: - ordering = ("examination",) + def merge_into(self, examination_to_merge_into: Self, delete: bool=True) -> bool: + """Merges the current examination into another + + To do so it replaces all associations (many to many and foreign key relations) + + Returns True if successful + """ + # Get all atlas series with the examination + atlas_series = self.atlas_series_examination.all() + longs_series = self.series_examination.all() + anatomy_questions = self.anatomyquestion_set.all() + + # Reassign them to the new object + for series in atlas_series: + series.examination = examination_to_merge_into + series.save() + + for series in longs_series: + series.examination = examination_to_merge_into + series.save() + + for question in anatomy_questions: + question.examination = examination_to_merge_into + question.save() + + # quick check to make sure nothing has gone wrong and we have hanging + # associations + querysets = ( + self.atlas_series_examination.all(), + self.series_examination.all(), + self.anatomyquestion_set.all(), + ) + + for q in querysets: + if q.count() > 0: + return False + + if delete: + self.delete() + return True class Site(models.Model): diff --git a/generic/templates/generic/examination_detail.html b/generic/templates/generic/examination_detail.html index 933b5755..4f3522ec 100755 --- a/generic/templates/generic/examination_detail.html +++ b/generic/templates/generic/examination_detail.html @@ -16,27 +16,48 @@

    Name: {{examination.examination}}

    Modality: {{examination.modality}} - {% if examination.atlas_series_examination_set.all %} -

    Associated Atlas Cases

    + {% if examination.atlas_series_examination.all %} +

    Associated Atlas Series

    {% endif %} - {% if examination.longs_series_examination_set.all %} -

    Associated Longs Cases

    + {% if examination.series_examination.all %} +

    Associated Longs Series

    {% endif %} +
    Merge + +
    + {{merge_form}} +

    This will replace all occurances of the "{{examination}}" and with the selected examination and delete it.

    +

    It cannot be undone

    + +
    + +
    +

    +
    + +
    + + {% endblock %} {% block js %} diff --git a/generic/templates/generic/examination_merge.html b/generic/templates/generic/examination_merge.html new file mode 100644 index 00000000..5ef51909 --- /dev/null +++ b/generic/templates/generic/examination_merge.html @@ -0,0 +1,14 @@ +{% if not form.is_valid %} + Please enter a number +{% elif examination_to_merge_into %} + + {% if success %} + Merged into {{examination_to_merge_into}} successfully + + {% else %} + There was an error merging into {{examination_to_merge_into}} + + {% endif %} +{% else %} + Examination to merge into is invalid: {{examination_to_merge_into}} +{% endif %} \ No newline at end of file diff --git a/generic/urls.py b/generic/urls.py index ddde4b7b..cc67f412 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path("examination/", views.examination_detail, name="examination_detail"), path("examination//delete", views.ExaminationDelete.as_view(), name="examination_delete"), path("examination//update", views.ExaminationUpdate.as_view(), name="examination_update"), + path("examination//merge", views.examination_merge, name="examination_merge"), path("examination/create/", views.create_examination, name="create_examination"), path( "examination/ajax/get_examination_id", diff --git a/generic/views.py b/generic/views.py index 619fe967..e0c592af 100644 --- a/generic/views.py +++ b/generic/views.py @@ -54,6 +54,7 @@ from .forms import ( CidUserExamForm, ExaminationForm, CidUserGroupForm, + ExaminationMergeForm, SupervisorForm, UserUserForm, UserUserGroupForm, @@ -178,22 +179,59 @@ def create_examination(request): request, "rapids/create_simple.html", {"form": form, "name": "Examination"} ) + @login_required @user_passes_test(lambda u: u.is_superuser) def examination_detail(request, pk): examination = get_object_or_404(Examination, pk=pk) + merge_form = ExaminationMergeForm() # logging.debug(atlas.subspecialty.first().name.all()) return render( request, "generic/examination_detail.html", + {"examination": examination, "merge_form": merge_form}, + ) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def examination_merge(request, pk): + examination = get_object_or_404(Examination, pk=pk) + + form = ExaminationMergeForm(request.POST) + + if form.is_valid(): + examination_to_merge_into = form.cleaned_data["examination_to_merge_into"] + + success = False + if examination.merge_into(examination_to_merge_into): + success = True + + + else: + examination_to_merge_into = False + return render( + request, + "generic/examination_merge.html", + { + "form": form, + "examination": examination, + "examination_to_merge_into": examination_to_merge_into, + "success": success + }, + ) + + # logging.debug(atlas.subspecialty.first().name.all()) + return render( + request, + "generic/examination_merge.html", { "examination": examination, }, ) - @csrf_exempt def get_examination_id(request): if request.accepts("application/json")(): @@ -818,17 +856,17 @@ class ExamViews(View, LoginRequiredMixin): .prefetch_related( Prefetch( "series", - queryset=LongSeries.objects.select_related("modality", "examination", "plane", "contrast").prefetch_related( - Prefetch("images") - ), - ), - "author" - #to_attr="prefetched_primary_answer" - # queryset=self.Answer.objects.filter(), - #"series", - #"abnormality", - #"region", - #"examination", + queryset=LongSeries.objects.select_related( + "modality", "examination", "plane", "contrast" + ).prefetch_related(Prefetch("images")), + ), + "author" + # to_attr="prefetched_primary_answer" + # queryset=self.Answer.objects.filter(), + # "series", + # "abnormality", + # "region", + # "examination", ) .all() ) @@ -849,7 +887,7 @@ class ExamViews(View, LoginRequiredMixin): "abnormality", "region", "examination", - "author" + "author", ) ) # questions = ( @@ -1835,7 +1873,7 @@ class ExamViews(View, LoginRequiredMixin): 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 @@ -1872,7 +1910,6 @@ class ExamViews(View, LoginRequiredMixin): case _: max_score = len(questions) - template_context = { "exam": exam, "cid": cid, @@ -2092,7 +2129,7 @@ class ExamViews(View, LoginRequiredMixin): exam.save() - missing_user_ids = valid_user_users - user_ids + missing_user_ids = valid_user_users - user_ids missing_users = [] if missing_user_ids: @@ -2971,6 +3008,7 @@ class SupervisorCreate(CidManagerRequiredMixin, CreateView): class SupervisorList(CidManagerRequiredMixin, ListView): model = Supervisor + class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView): model = Examination table_class = ExaminationTable @@ -2987,4 +3025,4 @@ class ExaminationDelete(RevisionMixin, SuperuserRequiredMixin, DeleteView): class ExaminationUpdate(UpdateView, SuperuserRequiredMixin): model = Examination - form_class = ExaminationForm \ No newline at end of file + form_class = ExaminationForm diff --git a/longs/models.py b/longs/models.py index 2378f0bd..2d063d86 100644 --- a/longs/models.py +++ b/longs/models.py @@ -399,6 +399,9 @@ class LongSeries(SeriesBase): def get_absolute_url(self): return reverse("longs:series_detail", kwargs={"pk": self.pk}) + def get_link(self): + return format_html("{}", self.get_absolute_url(), self) + class LongCreationDefault(models.Model): # author = models.OneToOneField(User, diff --git a/longs/tables.py b/longs/tables.py index acd7a72d..6ebc55a1 100755 --- a/longs/tables.py +++ b/longs/tables.py @@ -71,7 +71,7 @@ class LongSeriesTable(tables.Table): "longs:long_series_update", text="Edit", args=[A("pk")], orderable=False ) view = tables.LinkColumn( - "longs:long_series_detail", text="View", args=[A("pk")], orderable=False + "longs:series_detail", text="View", args=[A("pk")], orderable=False ) popup = tables.Column(empty_values=(), orderable=False) diff --git a/longs/templates/longs/long_display_block.html b/longs/templates/longs/long_display_block.html index 590b5fb1..df211522 100755 --- a/longs/templates/longs/long_display_block.html +++ b/longs/templates/longs/long_display_block.html @@ -14,7 +14,7 @@ Series {{ forloop.counter }}: - + {{series.get_block}} diff --git a/longs/templates/longs/mark_answer.html b/longs/templates/longs/mark_answer.html index b8711e4c..8637822a 100644 --- a/longs/templates/longs/mark_answer.html +++ b/longs/templates/longs/mark_answer.html @@ -65,7 +65,7 @@ Series {{ forloop.counter }}: - + {{series.get_block}} diff --git a/longs/urls.py b/longs/urls.py index 249d2151..5009dbf1 100755 --- a/longs/urls.py +++ b/longs/urls.py @@ -11,7 +11,7 @@ urlpatterns = [ path("author/", views.author_list, name="author_list"), path("question/", views.LongView.as_view(), name="long_view"), path("series/", views.LongSeriesView.as_view(), name="long_series_view"), - path("series/", views.long_series_detail, name="long_series_detail"), + path("series/", views.series_detail, name="series_detail"), path( "series//order_dicom", views.long_series_order_dicom, diff --git a/longs/views.py b/longs/views.py index e8941930..55aca0a2 100755 --- a/longs/views.py +++ b/longs/views.py @@ -149,7 +149,7 @@ def question_detail(request, pk): @login_required @user_is_author_or_long_series_checker_or_long_marker -def long_series_detail(request, pk): +def series_detail(request, pk): series = get_object_or_404(LongSeries, pk=pk) # if request.user not in long.author.all(): @@ -1080,7 +1080,7 @@ def long_series_order_dicom(request, pk): except: return HttpResponse("

    Series does not appear to contain dicoms

    ") - return redirect("longs:long_series_detail", pk=pk) + return redirect("longs:series_detail", pk=pk) @login_required @@ -1094,7 +1094,7 @@ def long_series_order_dicom_instance(request, pk): "

    Series does not appear to contain dicoms (or field InstanceNumber)

    " ) - return redirect("longs:long_series_detail", pk=pk) + return redirect("longs:series_detail", pk=pk) @login_required @@ -1108,7 +1108,7 @@ def long_series_order_dicom_SeriesInstanceUID(request, pk): "

    Series does not appear to contain dicoms (or field SeriesInstanceUID)

    " ) - return redirect("longs:long_series_detail", pk=pk) + return redirect("longs:series_detail", pk=pk) @login_required @@ -1117,7 +1117,7 @@ def long_series_order_upload_filename(request, pk): series = get_object_or_404(LongSeries, pk=pk) series.order_by_upload_filename() - return redirect("longs:long_series_detail", pk=pk) + return redirect("longs:series_detail", pk=pk) GenericExamViews = ExamViews(Exam, Long, None, UserAnswer, "longs", "long")