Add Procedure model and integrate into CaseForm; update views and templates for procedure handling
This commit is contained in:
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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
@@ -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/",
|
||||
|
||||
@@ -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 !
|
||||
|
||||
Reference in New Issue
Block a user