allow merging of examinations

This commit is contained in:
Ross
2023-07-03 11:10:50 +01:00
parent 1ade42d7ba
commit 6b5c3f8914
14 changed files with 178 additions and 39 deletions
+5 -1
View File
@@ -304,6 +304,7 @@ class Case(models.Model):
return authors return authors
def get_link(self): def get_link(self):
return f"{self.pk}: {self.title}"
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), str(self)) return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), str(self))
def __str__(self): def __str__(self):
@@ -391,7 +392,7 @@ class Series(SeriesBase):
def get_full_str(self): def get_full_str(self):
if self.case: 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 # case_id = self.case.pk
else: else:
case_id = "None" case_id = "None"
@@ -403,6 +404,9 @@ class Series(SeriesBase):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("atlas:series_detail", kwargs={"pk": self.pk}) return reverse("atlas:series_detail", kwargs={"pk": self.pk})
def get_link(self):
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.get_full_str())
class CaseCollection(ExamCollectionGenericBase): class CaseCollection(ExamCollectionGenericBase):
@@ -9,4 +9,9 @@
<li><a href="{% url 'atlas:presentation_view' %}">Presentations</li></a> <li><a href="{% url 'atlas:presentation_view' %}">Presentations</li></a>
<li><a href="{% url 'atlas:pathological_process_view' %}">Pathological Process</li></a> <li><a href="{% url 'atlas:pathological_process_view' %}">Pathological Process</li></a>
</ul> </ul>
{% if request.user.is_staff %}
<p>Manage <a href="{% url 'generic:examination_view' %}">Examinations</a>
</p>
{% endif %}
{% endblock %} {% endblock %}
+2
View File
@@ -128,6 +128,8 @@ class ExaminationForm(ModelForm):
model = Examination model = Examination
fields = ["examination", "modality"] fields = ["examination", "modality"]
class ExaminationMergeForm(Form):
examination_to_merge_into = ModelChoiceField(queryset=Examination.objects.all())
class QuestionNoteForm(ModelForm): class QuestionNoteForm(ModelForm):
class Meta: class Meta:
+54 -3
View File
@@ -1,7 +1,7 @@
from collections import defaultdict from collections import defaultdict
import json import json
import os import os
from typing import Tuple from typing import Self, Tuple
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
@@ -72,15 +72,66 @@ class Contrast(models.Model):
class Examination(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) examination = models.CharField(max_length=200, unique=True)
modality = models.ForeignKey(Modality, on_delete=models.SET_NULL, null=True) modality = models.ForeignKey(Modality, on_delete=models.SET_NULL, null=True)
class Meta:
ordering = ("examination",)
def __str__(self): def __str__(self):
return self.examination return self.examination
class Meta: def merge_into(self, examination_to_merge_into: Self, delete: bool=True) -> bool:
ordering = ("examination",) """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): class Site(models.Model):
@@ -16,27 +16,48 @@
<h3>Name: {{examination.examination}}</h3> <h3>Name: {{examination.examination}}</h3>
Modality: {{examination.modality}} Modality: {{examination.modality}}
</div> </div>
{% if examination.atlas_series_examination_set.all %} {% if examination.atlas_series_examination.all %}
<h4>Associated Atlas Cases</h4> <h4>Associated Atlas Series</h4>
<ul> <ul>
{% for case in examination.atlas_series_examination_set.all %} {% for series in examination.atlas_series_examination.all %}
<li>{{case}}</li> <li>{{series.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if examination.longs_series_examination_set.all %} {% if examination.series_examination.all %}
<h4>Associated Longs Cases</h4> <h4>Associated Longs Series</h4>
<ul> <ul>
{% for case in examination.longs_series_examination_set.all %} {% for series in examination.series_examination.all %}
<li>{{case}}</li> <li>{{series.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<details><summary>Merge</summary>
<form hx-post="{% url 'generic:examination_merge' examination.pk %}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-confirm="Are you sure?"
hx-target="#result">
{{merge_form}}
<p>This will replace all occurances of the "{{examination}}" and with the selected examination and delete it.</p>
<p>It cannot be undone</p>
<button type="submit">
Merge
</button>
</form>
<section>
<p id="result"><em></em></p>
</section>
</details>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
@@ -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 %}
+1
View File
@@ -13,6 +13,7 @@ urlpatterns = [
path("examination/<int:pk>", views.examination_detail, name="examination_detail"), path("examination/<int:pk>", views.examination_detail, name="examination_detail"),
path("examination/<int:pk>/delete", views.ExaminationDelete.as_view(), name="examination_delete"), path("examination/<int:pk>/delete", views.ExaminationDelete.as_view(), name="examination_delete"),
path("examination/<int:pk>/update", views.ExaminationUpdate.as_view(), name="examination_update"), path("examination/<int:pk>/update", views.ExaminationUpdate.as_view(), name="examination_update"),
path("examination/<int:pk>/merge", views.examination_merge, name="examination_merge"),
path("examination/create/", views.create_examination, name="create_examination"), path("examination/create/", views.create_examination, name="create_examination"),
path( path(
"examination/ajax/get_examination_id", "examination/ajax/get_examination_id",
+44 -6
View File
@@ -54,6 +54,7 @@ from .forms import (
CidUserExamForm, CidUserExamForm,
ExaminationForm, ExaminationForm,
CidUserGroupForm, CidUserGroupForm,
ExaminationMergeForm,
SupervisorForm, SupervisorForm,
UserUserForm, UserUserForm,
UserUserGroupForm, UserUserGroupForm,
@@ -178,22 +179,59 @@ def create_examination(request):
request, "rapids/create_simple.html", {"form": form, "name": "Examination"} request, "rapids/create_simple.html", {"form": form, "name": "Examination"}
) )
@login_required @login_required
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
def examination_detail(request, pk): def examination_detail(request, pk):
examination = get_object_or_404(Examination, pk=pk) examination = get_object_or_404(Examination, pk=pk)
merge_form = ExaminationMergeForm()
# logging.debug(atlas.subspecialty.first().name.all()) # logging.debug(atlas.subspecialty.first().name.all())
return render( return render(
request, request,
"generic/examination_detail.html", "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, "examination": examination,
}, },
) )
@csrf_exempt @csrf_exempt
def get_examination_id(request): def get_examination_id(request):
if request.accepts("application/json")(): if request.accepts("application/json")():
@@ -818,9 +856,9 @@ class ExamViews(View, LoginRequiredMixin):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"series", "series",
queryset=LongSeries.objects.select_related("modality", "examination", "plane", "contrast").prefetch_related( queryset=LongSeries.objects.select_related(
Prefetch("images") "modality", "examination", "plane", "contrast"
), ).prefetch_related(Prefetch("images")),
), ),
"author" "author"
# to_attr="prefetched_primary_answer" # to_attr="prefetched_primary_answer"
@@ -849,7 +887,7 @@ class ExamViews(View, LoginRequiredMixin):
"abnormality", "abnormality",
"region", "region",
"examination", "examination",
"author" "author",
) )
) )
# questions = ( # questions = (
@@ -1872,7 +1910,6 @@ class ExamViews(View, LoginRequiredMixin):
case _: case _:
max_score = len(questions) max_score = len(questions)
template_context = { template_context = {
"exam": exam, "exam": exam,
"cid": cid, "cid": cid,
@@ -2971,6 +3008,7 @@ class SupervisorCreate(CidManagerRequiredMixin, CreateView):
class SupervisorList(CidManagerRequiredMixin, ListView): class SupervisorList(CidManagerRequiredMixin, ListView):
model = Supervisor model = Supervisor
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView): class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
model = Examination model = Examination
table_class = ExaminationTable table_class = ExaminationTable
+3
View File
@@ -399,6 +399,9 @@ class LongSeries(SeriesBase):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("longs:series_detail", kwargs={"pk": self.pk}) return reverse("longs:series_detail", kwargs={"pk": self.pk})
def get_link(self):
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self)
class LongCreationDefault(models.Model): class LongCreationDefault(models.Model):
# author = models.OneToOneField(User, # author = models.OneToOneField(User,
+1 -1
View File
@@ -71,7 +71,7 @@ class LongSeriesTable(tables.Table):
"longs:long_series_update", text="Edit", args=[A("pk")], orderable=False "longs:long_series_update", text="Edit", args=[A("pk")], orderable=False
) )
view = tables.LinkColumn( 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) popup = tables.Column(empty_values=(), orderable=False)
@@ -14,7 +14,7 @@
<span class="series-block"> <span class="series-block">
<span> <span>
<span class="series-block-series-number">Series {{ forloop.counter }}:</span> <span class="series-block-series-number">Series {{ forloop.counter }}:</span>
<a href="{% url 'longs:long_series_detail' pk=series.pk %}"> <a href="{% url 'longs:series_detail' pk=series.pk %}">
{{series.get_block}} {{series.get_block}}
</a> </a>
+1 -1
View File
@@ -65,7 +65,7 @@
<span class="series-block"> <span class="series-block">
<span> <span>
Series {{ forloop.counter }}: Series {{ forloop.counter }}:
<a href="{% url 'longs:long_series_detail' pk=series.pk %}"> <a href="{% url 'longs:series_detail' pk=series.pk %}">
{{series.get_block}} {{series.get_block}}
</a> </a>
+1 -1
View File
@@ -11,7 +11,7 @@ urlpatterns = [
path("author/", views.author_list, name="author_list"), path("author/", views.author_list, name="author_list"),
path("question/", views.LongView.as_view(), name="long_view"), path("question/", views.LongView.as_view(), name="long_view"),
path("series/", views.LongSeriesView.as_view(), name="long_series_view"), path("series/", views.LongSeriesView.as_view(), name="long_series_view"),
path("series/<int:pk>", views.long_series_detail, name="long_series_detail"), path("series/<int:pk>", views.series_detail, name="series_detail"),
path( path(
"series/<int:pk>/order_dicom", "series/<int:pk>/order_dicom",
views.long_series_order_dicom, views.long_series_order_dicom,
+5 -5
View File
@@ -149,7 +149,7 @@ def question_detail(request, pk):
@login_required @login_required
@user_is_author_or_long_series_checker_or_long_marker @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) series = get_object_or_404(LongSeries, pk=pk)
# if request.user not in long.author.all(): # if request.user not in long.author.all():
@@ -1080,7 +1080,7 @@ def long_series_order_dicom(request, pk):
except: except:
return HttpResponse("<h1>Series does not appear to contain dicoms</h1>") return HttpResponse("<h1>Series does not appear to contain dicoms</h1>")
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:series_detail", pk=pk)
@login_required @login_required
@@ -1094,7 +1094,7 @@ def long_series_order_dicom_instance(request, pk):
"<h1>Series does not appear to contain dicoms (or field InstanceNumber)</h1>" "<h1>Series does not appear to contain dicoms (or field InstanceNumber)</h1>"
) )
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:series_detail", pk=pk)
@login_required @login_required
@@ -1108,7 +1108,7 @@ def long_series_order_dicom_SeriesInstanceUID(request, pk):
"<h1>Series does not appear to contain dicoms (or field SeriesInstanceUID)</h1>" "<h1>Series does not appear to contain dicoms (or field SeriesInstanceUID)</h1>"
) )
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:series_detail", pk=pk)
@login_required @login_required
@@ -1117,7 +1117,7 @@ def long_series_order_upload_filename(request, pk):
series = get_object_or_404(LongSeries, pk=pk) series = get_object_or_404(LongSeries, pk=pk)
series.order_by_upload_filename() 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") GenericExamViews = ExamViews(Exam, Long, None, UserAnswer, "longs", "long")