Add Procedure model and integrate into CaseForm; update views and templates for procedure handling

This commit is contained in:
Ross
2026-02-16 14:04:37 +00:00
parent c61f92ae9e
commit a51a5c8d54
7 changed files with 141 additions and 1 deletions
+5
View File
@@ -617,6 +617,7 @@ class CaseForm(ModelForm):
"presentation", "presentation",
"discussion", "discussion",
"report", "report",
"procedures",
"condition", "condition",
"pathological_process", "pathological_process",
"open_access", "open_access",
@@ -649,6 +650,7 @@ class CaseForm(ModelForm):
"presentation", "presentation",
"discussion", "discussion",
"condition", "condition",
"procedures",
"pathological_process", "pathological_process",
"report", "report",
"open_access", "open_access",
@@ -677,6 +679,9 @@ class CaseForm(ModelForm):
"subspecialty": CheckboxSelectMultiple(), "subspecialty": CheckboxSelectMultiple(),
"pathological_process": CheckboxSelectMultiple(), "pathological_process": CheckboxSelectMultiple(),
"previous_case": CaseSelect(), "previous_case": CaseSelect(),
"procedures": autocomplete.ModelSelect2Multiple(
url="atlas:procedure-autocomplete"
),
} }
def clean_cimar_uuid(self): def clean_cimar_uuid(self):
@@ -0,0 +1,30 @@
# Generated by Django 6.0.1 on 2026-02-16 14:01
import atlas.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('atlas', '0092_normalcase_author'),
]
operations = [
migrations.CreateModel(
name='Procedure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('canonical', models.ForeignKey(blank=True, help_text='If set, this Procedure is an alias and points to the canonical Procedure.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='aliases', to='atlas.procedure')),
('subspecialty', models.ManyToManyField(blank=True, to='atlas.subspecialty')),
],
bases=(atlas.models.SynMixin, models.Model),
),
migrations.AddField(
model_name='case',
name='procedures',
field=models.ManyToManyField(blank=True, help_text='Procedure(s) or operations the patient has undergone.', to='atlas.procedure'),
),
]
+37
View File
@@ -396,6 +396,35 @@ class PathologicalProcess(models.Model):
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.name) return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.name)
class Procedure(SynMixin, models.Model):
"""A procedure or operation performed on the patient (attachable to Cases).
This mirrors the lightweight structure used by `Presentation` / `PathologicalProcess`.
"""
name = models.CharField(max_length=255, unique=True)
# Optional canonical/alias pointer for future synonym handling
canonical = models.ForeignKey(
"self",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="aliases",
help_text="If set, this Procedure is an alias and points to the canonical Procedure.",
)
subspecialty = models.ManyToManyField("subspecialty", blank=True)
def __str__(self) -> str:
return self.name
def get_absolute_url(self):
return reverse("atlas:procedure_detail", kwargs={"pk": self.pk})
def get_link(self):
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.name)
class Differential(models.Model): class Differential(models.Model):
condition = models.ForeignKey(Condition, on_delete=models.CASCADE) condition = models.ForeignKey(Condition, on_delete=models.CASCADE)
case = models.ForeignKey( case = models.ForeignKey(
@@ -484,6 +513,13 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
help_text="The presentation(s) the case is associated with.", help_text="The presentation(s) the case is associated with.",
) )
# Procedures performed on the patient (e.g. operations, interventions)
procedures = models.ManyToManyField(
"Procedure",
blank=True,
help_text="Procedure(s) or operations the patient has undergone.",
)
pathological_process = models.ManyToManyField(PathologicalProcess, blank=True) pathological_process = models.ManyToManyField(PathologicalProcess, blank=True)
differential = models.ManyToManyField( differential = models.ManyToManyField(
@@ -830,6 +866,7 @@ class NormalCase(models.Model, AuthorMixin):
notes = models.TextField(null=True, blank=True) notes = models.TextField(null=True, blank=True)
class Meta: class Meta:
ordering = ["-added_date"] ordering = ["-added_date"]
@@ -302,6 +302,18 @@
<li>{{con.get_link}}</li> <li>{{con.get_link}}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<div>
<strong>Procedure(s):</strong>
{% if case.procedures.all %}
<ul>
{% for proc in case.procedures.all %}
<li>{{ proc.get_link }}</li>
{% endfor %}
</ul>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
<div> <div>
<details open> <details open>
<summary><b>Findings</b></summary> <summary><b>Findings</b></summary>
@@ -0,0 +1,15 @@
{% extends 'atlas/base.html' %}
{% block content %}
<div class="container">
<h1>{{ procedure.name }}</h1>
<p>Cases with this procedure:</p>
<ul>
{% for case in cases %}
<li>{{ case.get_link }}</li>
{% empty %}
<li class="text-muted">No cases found</li>
{% endfor %}
</ul>
</div>
{% endblock %}
+7 -1
View File
@@ -2,7 +2,7 @@ from django.urls import path, include
from django.views.generic import RedirectView, TemplateView from django.views.generic import RedirectView, TemplateView
from atlas.models import Condition, Finding, Presentation, Structure, Subspecialty from atlas.models import Condition, Finding, Presentation, Structure, Subspecialty, Procedure
from . import views from . import views
app_name = "atlas" app_name = "atlas"
@@ -628,6 +628,11 @@ urlpatterns = [
views.StructureAutocomplete.as_view(model=Structure, create_field="name", validate_create=True), views.StructureAutocomplete.as_view(model=Structure, create_field="name", validate_create=True),
name="structure-autocomplete", name="structure-autocomplete",
), ),
path(
"procedure-autocomplete",
views.ProcedureAutocomplete.as_view(model=Procedure, create_field="name"),
name="procedure-autocomplete",
),
path( path(
"subspecialty-autocomplete", "subspecialty-autocomplete",
views.SubspecialtyAutocomplete.as_view(model=Subspecialty, create_field="name"), views.SubspecialtyAutocomplete.as_view(model=Subspecialty, create_field="name"),
@@ -637,6 +642,7 @@ urlpatterns = [
path( path(
"presentation/<int:pk>", views.presentation_detail, name="presentation_detail" "presentation/<int:pk>", views.presentation_detail, name="presentation_detail"
), ),
path("procedure/<int:pk>", views.procedure_detail, name="procedure_detail"),
path("presentation/create", views.PresentationCreate.as_view(), name="presentation_create"), path("presentation/create", views.PresentationCreate.as_view(), name="presentation_create"),
path( path(
"process/", "process/",
+35
View File
@@ -118,6 +118,7 @@ from .models import (
UserReportAnswer, UserReportAnswer,
PrerequisiteRequired PrerequisiteRequired
) )
from .models import Procedure
from .models import NormalCase from .models import NormalCase
from .tables import ( from .tables import (
CaseCollectionTable, CaseCollectionTable,
@@ -590,6 +591,7 @@ def get_case_for_case_detail(pk: int) -> Case:
), ),
Prefetch("subspecialty", queryset=Subspecialty.objects.all()), Prefetch("subspecialty", queryset=Subspecialty.objects.all()),
Prefetch("condition", queryset=Condition.objects.all()), Prefetch("condition", queryset=Condition.objects.all()),
Prefetch("procedures", queryset=Procedure.objects.all()),
Prefetch("presentation", queryset=Presentation.objects.all()), Prefetch("presentation", queryset=Presentation.objects.all()),
Prefetch("pathological_process", queryset=PathologicalProcess.objects.all()), Prefetch("pathological_process", queryset=PathologicalProcess.objects.all()),
Prefetch("caseresource_set", queryset=CaseResource.objects.all()), Prefetch("caseresource_set", queryset=CaseResource.objects.all()),
@@ -1198,6 +1200,23 @@ def presentation_detail(request, pk):
) )
@login_required
def procedure_detail(request, pk):
procedure = get_object_or_404(Procedure, pk=pk)
# Cases that have this procedure
cases = Case.objects.filter(procedures__id=procedure.pk)
return render(
request,
"atlas/procedure_detail.html",
{
"procedure": procedure,
"cases": cases,
},
)
@login_required @login_required
# @user_is_atlas_editor # @user_is_atlas_editor
def pathological_process_detail(request, pk): def pathological_process_detail(request, pk):
@@ -2985,6 +3004,22 @@ class ConditionAutocomplete(autocomplete.Select2QuerySetView):
return qs return qs
class ProcedureAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return Procedure.objects.none()
qs = Procedure.objects.all()
if self.q:
try:
qs = qs.filter(name__icontains=self.q)
except FieldError:
return Procedure.objects.none()
return qs
class ConditionAutocompleteRCRCurriculum(autocomplete.Select2QuerySetView): class ConditionAutocompleteRCRCurriculum(autocomplete.Select2QuerySetView):
def get_queryset(self): def get_queryset(self):
# Don't forget to filter out results depending on the visitor ! # Don't forget to filter out results depending on the visitor !