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
- {% for case in examination.atlas_series_examination_set.all %}
- - {{case}}
+ {% for series in examination.atlas_series_examination.all %}
+ - {{series.get_link}}
{% endfor %}
{% endif %}
- {% if examination.longs_series_examination_set.all %}
- Associated Longs Cases
+ {% if examination.series_examination.all %}
+ Associated Longs Series
- {% for case in examination.longs_series_examination_set.all %}
- - {{case}}
+ {% for series in examination.series_examination.all %}
+ - {{series.get_link}}
{% endfor %}
{% endif %}
+ Merge
+
+
+
+
+
+
+
+
{% 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")