allow merging of examinations
This commit is contained in:
+6
-2
@@ -304,10 +304,11 @@ class Case(models.Model):
|
||||
return authors
|
||||
|
||||
def get_link(self):
|
||||
return f"{self.pk}: {self.title}"
|
||||
return format_html("<a href='{}'>{}</a>", 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("<a href='{}'>{}</a>", self.get_absolute_url(), self.get_full_str())
|
||||
|
||||
|
||||
|
||||
class CaseCollection(ExamCollectionGenericBase):
|
||||
|
||||
@@ -9,4 +9,9 @@
|
||||
<li><a href="{% url 'atlas:presentation_view' %}">Presentations</li></a>
|
||||
<li><a href="{% url 'atlas:pathological_process_view' %}">Pathological Process</li></a>
|
||||
</ul>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<p>Manage <a href="{% url 'generic:examination_view' %}">Examinations</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+54
-3
@@ -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):
|
||||
|
||||
@@ -16,27 +16,48 @@
|
||||
<h3>Name: {{examination.examination}}</h3>
|
||||
Modality: {{examination.modality}}
|
||||
</div>
|
||||
{% if examination.atlas_series_examination_set.all %}
|
||||
<h4>Associated Atlas Cases</h4>
|
||||
{% if examination.atlas_series_examination.all %}
|
||||
<h4>Associated Atlas Series</h4>
|
||||
<ul>
|
||||
|
||||
{% for case in examination.atlas_series_examination_set.all %}
|
||||
<li>{{case}}</li>
|
||||
{% for series in examination.atlas_series_examination.all %}
|
||||
<li>{{series.get_link}}</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if examination.longs_series_examination_set.all %}
|
||||
<h4>Associated Longs Cases</h4>
|
||||
{% if examination.series_examination.all %}
|
||||
<h4>Associated Longs Series</h4>
|
||||
<ul>
|
||||
|
||||
{% for case in examination.longs_series_examination_set.all %}
|
||||
<li>{{case}}</li>
|
||||
{% for series in examination.series_examination.all %}
|
||||
<li>{{series.get_link}}</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% 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 %}
|
||||
|
||||
{% 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 %}
|
||||
@@ -13,6 +13,7 @@ urlpatterns = [
|
||||
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>/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/ajax/get_examination_id",
|
||||
|
||||
+55
-17
@@ -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
|
||||
form_class = ExaminationForm
|
||||
|
||||
@@ -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("<a href='{}'>{}</a>", self.get_absolute_url(), self)
|
||||
|
||||
|
||||
class LongCreationDefault(models.Model):
|
||||
# author = models.OneToOneField(User,
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<span class="series-block">
|
||||
<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}}
|
||||
</a>
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<span class="series-block">
|
||||
<span>
|
||||
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}}
|
||||
</a>
|
||||
|
||||
|
||||
+1
-1
@@ -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/<int:pk>", views.long_series_detail, name="long_series_detail"),
|
||||
path("series/<int:pk>", views.series_detail, name="series_detail"),
|
||||
path(
|
||||
"series/<int:pk>/order_dicom",
|
||||
views.long_series_order_dicom,
|
||||
|
||||
+5
-5
@@ -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("<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
|
||||
@@ -1094,7 +1094,7 @@ def long_series_order_dicom_instance(request, pk):
|
||||
"<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
|
||||
@@ -1108,7 +1108,7 @@ def long_series_order_dicom_SeriesInstanceUID(request, pk):
|
||||
"<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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user