enable priors in atlas

This commit is contained in:
Ross
2024-07-17 22:00:57 +01:00
parent 35e4b81e34
commit 7075bba7a2
8 changed files with 312 additions and 6 deletions
+22 -1
View File
@@ -780,4 +780,25 @@ class QuestionSchemaForm(ModelForm):
widgets = { widgets = {
"schema": SvelteJSONEditorWidgetOverride(), "schema": SvelteJSONEditorWidgetOverride(),
} }
class PriorCaseForm(Form):
relation = CharField(max_length=255, required=True)
#def __init__(self, *args, **kwargs):
# self.case_detail = kwargs.pop(
# "case_detail"
# ) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
# super(PriorCaseForm, self).__init__(*args, **kwargs)
# prior_cases = self.case_detail.case.get_all_prior_cases()
# if not prior_cases:
# prior_cases = Case.objects.none()
# self.fields["case"] = ChoiceField(
# required=False,
# # widget=Select(verbose_name="Series", is_stacked=False),
# )
+26
View File
@@ -0,0 +1,26 @@
# Generated by Django 5.0.2 on 2024-07-15 11:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0055_questionschema'),
]
operations = [
migrations.CreateModel(
name='CasePrior',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('relation_text', models.CharField(blank=True, help_text='Text to describe the relationship between the cases', max_length=255)),
('case_detail', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.casedetail')),
('prior_case', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='prior_case', to='atlas.case')),
],
options={
'unique_together': {('case_detail', 'prior_case')},
},
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-07-15 12:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0056_caseprior'),
]
operations = [
migrations.AlterField(
model_name='caseprior',
name='prior_case',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prior_case', to='atlas.case'),
),
]
+26
View File
@@ -461,6 +461,18 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
def get_viva_details_json(self): def get_viva_details_json(self):
return json.dumps(self.get_viva_details()) return json.dumps(self.get_viva_details())
def get_all_prior_cases(self):
prior_cases = []
while self.previous_case is not None:
prior_cases.append(self.previous_case)
self = self.previous_case
return prior_cases
def get_series(self):
return self.series.all().prefetch_related("images", "examination", "plane")
def extract_image_dicom_json_from_ds(ds, url, image_index): def extract_image_dicom_json_from_ds(ds, url, image_index):
to_keep = [ to_keep = [
@@ -1007,6 +1019,9 @@ class CaseDetail(models.Model):
class Meta: class Meta:
ordering = ("sort_order",) ordering = ("sort_order",)
def __str__(self) -> str:
return f"{self.case} -> {self.collection}"
def get_question_schema(self): def get_question_schema(self):
return json.dumps(self.question_schema) return json.dumps(self.question_schema)
@@ -1014,6 +1029,17 @@ class CaseDetail(models.Model):
"""Returns the correct question answers as a json string""" """Returns the correct question answers as a json string"""
return json.dumps(self.question_answers) return json.dumps(self.question_answers)
class CasePrior(models.Model):
case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case")
relation_text = models.CharField(max_length=255, blank=True, help_text="Text to describe the relationship between the cases")
class Meta:
unique_together = ("case_detail", "prior_case")
def __str__(self) -> str:
return f"{self.case_detail.case} -> {self.prior_case}"
class BaseReportAnswer(models.Model): class BaseReportAnswer(models.Model):
question = models.ForeignKey(CaseDetail, on_delete=models.CASCADE) question = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
@@ -0,0 +1,101 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<div>
{% if previous %}
<a href="{% url 'atlas:collection_case_details' collection.id previous.id %}">Previous question</a>
{% endif %}
Viewing question as part of collection: <a href="{% url 'atlas:collection_detail' collection.id %}">{{collection.name}}</a> [{{case_number|add:1}}/{{collection_length}}]
{% if next %}
<a href="{% url 'atlas:collection_case_details' collection.id next.id %}">Next question</a>
{% endif %}
</div>
<h2>Case: <a href="{% url 'atlas:case_detail' case_detail.case.pk %}">{{case_detail.case.title}}</a></h2>
<p>This page allows you to add series from prior cases to a case in a collection. In order to be available here the cases must be linked on the system.</p>
<p>It is necessary to give a description of how the cases are related that will be displayed to the user when viewing the case.</p>
<h3>Available priors</h3>
<ul>
{% for case, added, relation in available_priors %}
<li class='
{% if added %}
added
{% else %}
not-added
{% endif %}
' data-case={{case.id}}>
{{case.title}} / {{added}} / {{relation}}
<br/>{{case.get_series_blocks|safe}}
<br/>
{% if not added %}
<form hx-post="{% url 'atlas:collection_case_priors' collection.id case_detail.case.pk %}"
hx-target="#htmx-response-{{case.id}}"
_="on htmx:afterRequest log 'test' then remove me"
>
{% csrf_token %}
<input type="hidden" name="prior_case_id" value="{{case.id}}" />
<input type="text" name="relation" value="{{relation}}" placeholder="relation to case" />
<button class="btn primary">Add to case</button>
</form>
{% else %}
<form hx-post="{% url 'atlas:collection_case_priors' collection.id case_detail.case.pk %}"
hx-target="#htmx-response-{{case.id}}"
_="on htmx:afterRequest log 'test' then remove me"
>
Case available: {{relation}}
<input type="hidden" name="remove" value="{{case.id}}" />
<button class="btn primary">Remove from case</button>
</form>
{% endif %}
<span id="htmx-response-{{case.id}}"></span>
</li>
{% endfor %}
</ul>
{% comment %} <form method="POST">
{% csrf_token %}
{{ form }}
<button type="submit" value="save" name="submit">Submit</button>
</form> {% endcomment %}
{% comment %} <h2>Blank form</h2>
<form>
{{ blank_form}}
</form> {% endcomment %}
{% endblock %}
{% block css %}
<style>
.added {
border: 1px solid green;
}
.not-added {
border: 1px solid white;
}
</style>
{% endblock %}
{% block js %}
{% endblock %}
@@ -63,13 +63,21 @@
<summary> <summary>
Images Images
</summary> </summary>
{% for series in series_list %} {% for series, prior, relation in series_to_load %}
<span class="series-block"> <span class="series-block
{% if prior %}
prior
{% endif %}
">
<a href="#" onclick='window.loadDicomViewer(window.images[{{forloop.counter0}}])'> <a href="#" onclick='window.loadDicomViewer(window.images[{{forloop.counter0}}])'>
<span> <span>
<span class="series-block-series-number">Series {{ forloop.counter }}:</span> <span class="series-block-series-number">Series {{ forloop.counter }}:</span>
{{series.get_block}} {{series.get_block}}
</span> </span>
{% if prior %}
<br/><span class="relation">Prior: {{relation}}</span>
{% endif %}
</a> </a>
</span> </span>
{% endfor %} {% endfor %}
@@ -263,6 +271,10 @@
background-color: darkblue; background-color: darkblue;
} }
.series-block.prior {
border: darkgray solid 1px;
}
fieldset:disabled textarea { fieldset:disabled textarea {
background-color: black; background-color: black;
} }
@@ -291,6 +303,11 @@
.answer-block>div { .answer-block>div {
padding-top: 10px; padding-top: 10px;
} }
.relation {
opacity: 0.5;
font-size: 0.8em;
}
</style> </style>
{% endblock %} {% endblock %}
@@ -299,9 +316,13 @@
{% comment %} <script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script> {% endcomment %} {% comment %} <script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script> {% endcomment %}
<script type="text/javascript"> <script type="text/javascript">
window.images = { window.images = {
{% for series in series_list %} {% comment %} {% for series in series_list %}
{{ forloop.counter0 }}: ["{{ series.get_image_url_array_not_json }}"],
{% endfor %} {% endcomment %}
{% for series, prior, relation in series_to_load %}
{{ forloop.counter0 }}: ["{{ series.get_image_url_array_not_json }}"], {{ forloop.counter0 }}: ["{{ series.get_image_url_array_not_json }}"],
{% endfor %} {% endfor %}
} }
$(document).ready(function () { $(document).ready(function () {
+5
View File
@@ -116,6 +116,11 @@ urlpatterns = [
views.collection_case_details, views.collection_case_details,
name="collection_case_details", name="collection_case_details",
), ),
path(
"collection/<int:exam_id>/case/<int:case_id>/priors",
views.collection_case_priors,
name="collection_case_priors",
),
path( path(
"collection/<int:exam_id>/question_schemas", "collection/<int:exam_id>/question_schemas",
views.collection_question_schemas, views.collection_question_schemas,
+89 -2
View File
@@ -49,6 +49,7 @@ from .forms import (
ConditionForm, ConditionForm,
FindingForm, FindingForm,
JsonAnswerForm, JsonAnswerForm,
PriorCaseForm,
QuestionSchemaForm, QuestionSchemaForm,
ResourceForm, ResourceForm,
SelfReviewForm, SelfReviewForm,
@@ -66,6 +67,7 @@ from .models import (
Case, Case,
CaseCollection, CaseCollection,
CaseDetail, CaseDetail,
CasePrior,
CidReportAnswer, CidReportAnswer,
Condition, Condition,
Differential, Differential,
@@ -1631,6 +1633,78 @@ def collection_take_start(request, pk):
request, "atlas/collection_review_start.html", template_variables request, "atlas/collection_review_start.html", template_variables
) )
@user_is_collection_author_or_atlas_editor
def collection_case_priors(request, exam_id, case_id):
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
if request.htmx:
if "remove" in request.POST:
p = CasePrior.objects.get(case_detail=case_detail, prior_case=request.POST["remove"])
p.delete()
return HttpResponse(f"Case removed")
elif "prior_case_id" in request.POST:
prior_case = Case.objects.get(pk=request.POST["prior_case_id"])
p, created = CasePrior.objects.get_or_create(case_detail=case_detail, prior_case=prior_case)
p.relation_text=request.POST["relation"]
if not p.relation_text:
return HttpResponse("You need to enter text to describe the relationship between the cases")
p.save()
return HttpResponse(f"Case added")
else:
return HttpResponse("False")
raise Http404()
collection = case_detail.collection
added_priors = CasePrior.objects.filter(case_detail=case_detail)
added_relation_map = {}
for prior in added_priors:
added_relation_map[prior.prior_case] = prior.relation_text
case = case_detail.case
available_cases = case.get_all_prior_cases()
available_priors = []
for case in available_cases:
if case in added_relation_map:
available_priors.append((case, True, added_relation_map[case]))
else:
available_priors.append((case, False, ""))
form = PriorCaseForm()
case_number, case_count = collection.get_index_of_case(
case_detail.case, case_count=True
)
previous = collection.get_previous_case(case_detail.case)
next = collection.get_next_case(case_detail.case)
return render(
request,
"atlas/collection_case_priors.html",
{
"case_detail": case_detail,
"form": form,
#"example_form": example_form,
"collection": collection,
"case": case_detail.case,
"previous": previous,
"next": next,
"collection_length": case_count,
"case_number": case_number,
"added_priors": added_priors,
"available_priors": available_priors,
},
)
@user_is_collection_author_or_atlas_editor @user_is_collection_author_or_atlas_editor
def collection_case_details(request, exam_id, case_id): def collection_case_details(request, exam_id, case_id):
@@ -2039,8 +2113,8 @@ def collection_case_view_take(
cid_user_exam.start_time = timezone.now() cid_user_exam.start_time = timezone.now()
cid_user_exam.save() cid_user_exam.save()
case_detail = CaseDetail.objects.get(case=case, collection=collection)
if not collection.review_only(): if not collection.review_only():
case_detail = CaseDetail.objects.get(case=case, collection=collection)
if collection.collection_type == "REP": if collection.collection_type == "REP":
if cid is not None: if cid is not None:
answer = case_detail.cidreportanswer_set.filter(cid=cid).first() answer = case_detail.cidreportanswer_set.filter(cid=cid).first()
@@ -2116,7 +2190,19 @@ def collection_case_view_take(
series_list = case.series.all().prefetch_related("images", "examination", "plane") series_list = case.series.all().prefetch_related("images", "examination", "plane")
print("case_no") prior_cases = case_detail.caseprior_set.all()
series_to_load = []
for series in series_list:
series_to_load.append((series, False, ""))
for prior in prior_cases:
for series in prior.prior_case.get_series():
series_to_load.append((series, True, prior.relation_text))
previous = case_number > 0 previous = case_number > 0
next = case_number < (case_count - 1) next = case_number < (case_count - 1)
@@ -2160,6 +2246,7 @@ def collection_case_view_take(
"collection": collection, "collection": collection,
"case": case, "case": case,
"series_list": series_list, "series_list": series_list,
"series_to_load": series_to_load,
"case_number": case_number, "case_number": case_number,
"previous": previous, "previous": previous,
"next": next, "next": next,