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",
"discussion",
"report",
"procedures",
"condition",
"pathological_process",
"open_access",
@@ -649,6 +650,7 @@ class CaseForm(ModelForm):
"presentation",
"discussion",
"condition",
"procedures",
"pathological_process",
"report",
"open_access",
@@ -677,6 +679,9 @@ class CaseForm(ModelForm):
"subspecialty": CheckboxSelectMultiple(),
"pathological_process": CheckboxSelectMultiple(),
"previous_case": CaseSelect(),
"procedures": autocomplete.ModelSelect2Multiple(
url="atlas:procedure-autocomplete"
),
}
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)
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):
condition = models.ForeignKey(Condition, on_delete=models.CASCADE)
case = models.ForeignKey(
@@ -484,6 +513,13 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
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)
differential = models.ManyToManyField(
@@ -830,6 +866,7 @@ class NormalCase(models.Model, AuthorMixin):
notes = models.TextField(null=True, blank=True)
class Meta:
ordering = ["-added_date"]
@@ -302,6 +302,18 @@
<li>{{con.get_link}}</li>
{% endfor %}
</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>
<details open>
<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 atlas.models import Condition, Finding, Presentation, Structure, Subspecialty
from atlas.models import Condition, Finding, Presentation, Structure, Subspecialty, Procedure
from . import views
app_name = "atlas"
@@ -628,6 +628,11 @@ urlpatterns = [
views.StructureAutocomplete.as_view(model=Structure, create_field="name", validate_create=True),
name="structure-autocomplete",
),
path(
"procedure-autocomplete",
views.ProcedureAutocomplete.as_view(model=Procedure, create_field="name"),
name="procedure-autocomplete",
),
path(
"subspecialty-autocomplete",
views.SubspecialtyAutocomplete.as_view(model=Subspecialty, create_field="name"),
@@ -637,6 +642,7 @@ urlpatterns = [
path(
"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(
"process/",
+35
View File
@@ -118,6 +118,7 @@ from .models import (
UserReportAnswer,
PrerequisiteRequired
)
from .models import Procedure
from .models import NormalCase
from .tables import (
CaseCollectionTable,
@@ -590,6 +591,7 @@ def get_case_for_case_detail(pk: int) -> Case:
),
Prefetch("subspecialty", queryset=Subspecialty.objects.all()),
Prefetch("condition", queryset=Condition.objects.all()),
Prefetch("procedures", queryset=Procedure.objects.all()),
Prefetch("presentation", queryset=Presentation.objects.all()),
Prefetch("pathological_process", queryset=PathologicalProcess.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
# @user_is_atlas_editor
def pathological_process_detail(request, pk):
@@ -2985,6 +3004,22 @@ class ConditionAutocomplete(autocomplete.Select2QuerySetView):
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):
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !