From 44d23801b2f5751ab2dd8970b805d426582296c3 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 16 Feb 2026 14:15:28 +0000 Subject: [PATCH] Add Procedure model integration: create views, forms, and templates; update URLs and search functionality --- atlas/admin.py | 2 + atlas/forms.py | 12 ++-- atlas/tables.py | 21 ++++++ .../atlas/_categories_search_results.html | 4 +- atlas/templates/atlas/base.html | 2 + atlas/templates/atlas/categories_list.html | 10 +++ atlas/templates/atlas/procedure_form.html | 23 +++++++ atlas/urls.py | 5 ++ atlas/views.py | 68 ++++++++++++++++++- 9 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 atlas/templates/atlas/procedure_form.html diff --git a/atlas/admin.py b/atlas/admin.py index 155938a0..3b4f4e86 100755 --- a/atlas/admin.py +++ b/atlas/admin.py @@ -15,6 +15,7 @@ from .models import ( Structure, PathologicalProcess, Presentation, + Procedure, UncategorisedDicom, SeriesDetail, Resource, @@ -51,6 +52,7 @@ admin.site.register(UncategorisedDicom) admin.site.register(SeriesDetail) admin.site.register(Resource) admin.site.register(CaseDisplaySet) +admin.site.register(Procedure) admin.site.register(UserReportAnswer) admin.site.register(CidReportAnswer) diff --git a/atlas/forms.py b/atlas/forms.py index 830af729..f5f65a62 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -42,6 +42,7 @@ from atlas.models import ( Subspecialty, Presentation, PathologicalProcess, + Procedure, UncategorisedDicom, UserReportAnswer, CaseDisplaySet, @@ -395,6 +396,12 @@ class PathologicalProcessForm(ModelForm): exclude = [] +class ProcedureForm(ModelForm): + class Meta: + model = Procedure + exclude = [] + + class SeriesFindingForm(ModelForm): class Meta: model = SeriesFinding @@ -617,7 +624,6 @@ class CaseForm(ModelForm): "presentation", "discussion", "report", - "procedures", "condition", "pathological_process", "open_access", @@ -650,7 +656,6 @@ class CaseForm(ModelForm): "presentation", "discussion", "condition", - "procedures", "pathological_process", "report", "open_access", @@ -679,9 +684,6 @@ class CaseForm(ModelForm): "subspecialty": CheckboxSelectMultiple(), "pathological_process": CheckboxSelectMultiple(), "previous_case": CaseSelect(), - "procedures": autocomplete.ModelSelect2Multiple( - url="atlas:procedure-autocomplete" - ), } def clean_cimar_uuid(self): diff --git a/atlas/tables.py b/atlas/tables.py index e5f15aef..0c331b37 100755 --- a/atlas/tables.py +++ b/atlas/tables.py @@ -15,6 +15,7 @@ from .models import ( Finding, Subspecialty, Resource, + Procedure, ) from django.utils.html import format_html @@ -395,6 +396,26 @@ class PathologicalProcessTable(SelectionTable): sequence = ("name",) +class ProcedureTable(SelectionTable): + name = tables.Column( + linkify=("atlas:procedure_detail", {"pk": tables.A("pk")}), + verbose_name="Procedure", + ) + + edit = tables.LinkColumn( + "atlas:procedure_update", text="Edit", args=[A("pk")], orderable=False + ) + delete = tables.LinkColumn( + "atlas:procedure_delete", text="Delete", args=[A("pk")], orderable=False + ) + + class Meta(SelectionTable.Meta): + model = Procedure + template_name = "django_tables2/bootstrap4.html" + fields = ("name",) + sequence = ("name",) + + class SubspecialtyTable(SelectionTable): name = tables.Column( linkify=("atlas:subspecialty_detail", {"pk": tables.A("pk")}), diff --git a/atlas/templates/atlas/_categories_search_results.html b/atlas/templates/atlas/_categories_search_results.html index 29cb585f..2bd84d40 100644 --- a/atlas/templates/atlas/_categories_search_results.html +++ b/atlas/templates/atlas/_categories_search_results.html @@ -18,6 +18,8 @@ {{ item.name }} {% elif group_name == 'presentations' %} {{ item.name }} + {% elif group_name == 'procedures' %} + {{ item.name }} {% else %} {{ item.name }} {% endif %} @@ -27,7 +29,7 @@ {% endif %} {% endfor %} - {% if not results.conditions.exists and not results.findings.exists and not results.structures.exists and not results.subspecialties.exists and not results.presentations.exists %} + {% if not results.conditions.exists and not results.findings.exists and not results.structures.exists and not results.subspecialties.exists and not results.presentations.exists and not results.procedures.exists %}

No matches found.

{% endif %} diff --git a/atlas/templates/atlas/base.html b/atlas/templates/atlas/base.html index 8106488e..6c71be68 100755 --- a/atlas/templates/atlas/base.html +++ b/atlas/templates/atlas/base.html @@ -90,6 +90,7 @@
  • Subspecialties
  • Presentations
  • Pathological process
  • +
  • Procedures
  • {% if request.user.is_staff %}
  • @@ -99,6 +100,7 @@
  • New subspecialty
  • New presentation
  • New pathological process
  • +
  • New procedure
  • {% endif %} diff --git a/atlas/templates/atlas/categories_list.html b/atlas/templates/atlas/categories_list.html index bbf5449b..216ac68a 100644 --- a/atlas/templates/atlas/categories_list.html +++ b/atlas/templates/atlas/categories_list.html @@ -10,6 +10,7 @@
  • Subspecialties
  • Presentations
  • Pathological Process
  • +
  • Procedures
  • @@ -94,6 +95,15 @@ hx-indicator="#category-search-loading" />
    +
    + + +
    diff --git a/atlas/templates/atlas/procedure_form.html b/atlas/templates/atlas/procedure_form.html new file mode 100644 index 00000000..e4079dbb --- /dev/null +++ b/atlas/templates/atlas/procedure_form.html @@ -0,0 +1,23 @@ +{% extends "atlas/base.html" %} +{% load crispy_forms_tags %} + +{% block js %} + {{ form.media }} +{% endblock %} + +{% block content %} +

    Add / Edit Procedure

    +

    Use this form to create or edit a procedure.

    +

    Please check if it already exists before doing so!

    + +
    + {% csrf_token %} + {% crispy form %} + +
    + + Cancel +
    +
    + +{% endblock %} diff --git a/atlas/urls.py b/atlas/urls.py index cda6d538..26046407 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -642,7 +642,12 @@ urlpatterns = [ path( "presentation/", views.presentation_detail, name="presentation_detail" ), + path("procedure/", views.ProcedureView.as_view(), name="procedure_view"), path("procedure/", views.procedure_detail, name="procedure_detail"), + path("procedure/create", views.ProcedureCreate.as_view(), name="procedure_create"), + path("procedure//update", views.ProcedureUpdate.as_view(), name="procedure_update"), + path("procedure//delete", views.ProcedureDelete.as_view(), name="procedure_delete"), + path("presentation/create", views.PresentationCreate.as_view(), name="presentation_create"), path( "process/", diff --git a/atlas/views.py b/atlas/views.py index 05bcf7ee..9672b930 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -88,6 +88,7 @@ from .forms import ( SubspecialtyForm, PresentationForm, PathologicalProcessForm, + ProcedureForm, UserQuestionAnswerForm, UserReportAnswerForm, ) @@ -2748,6 +2749,65 @@ class PresentationView(LoginRequiredMixin, UserConfigurablePaginationMixin, Sing return ctx +class ProcedureView(LoginRequiredMixin, UserConfigurablePaginationMixin, SingleTableMixin, ListView): + model = Procedure + table_class = __import__('atlas.tables', fromlist=['ProcedureTable']).ProcedureTable + template_name = "atlas/view.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + try: + ctx["create_url"] = reverse(f"atlas:{self.model._meta.model_name}_create") + except NoReverseMatch: + ctx["create_url"] = None + ctx["model_verbose_name"] = getattr(self.model._meta, "verbose_name", None) + ctx["model_verbose_name_plural"] = getattr(self.model._meta, "verbose_name_plural", None) + return ctx + + +class ProcedureCreate(RevisionMixin, LoginRequiredMixin, CreateView): + model = Procedure + form_class = ProcedureForm + template_name = "atlas/procedure_form.html" + def form_valid(self, form): + self.object = form.save() + return super().form_valid(form) + + def form_invalid(self, form): + logger.warning("ProcedureCreate form invalid: %s", form.errors) + return super().form_invalid(form) + + def get_success_url(self): + try: + return reverse("atlas:procedure_detail", kwargs={"pk": self.object.pk}) + except Exception: + return reverse("atlas:procedure_view") + + +class ProcedureUpdate(RevisionMixin, LoginRequiredMixin, UpdateView): + model = Procedure + form_class = ProcedureForm + template_name = "atlas/procedure_form.html" + def form_valid(self, form): + self.object = form.save() + return super().form_valid(form) + + def form_invalid(self, form): + logger.warning("ProcedureUpdate form invalid: %s", form.errors) + return super().form_invalid(form) + + def get_success_url(self): + try: + return reverse("atlas:procedure_detail", kwargs={"pk": self.object.pk}) + except Exception: + return reverse("atlas:procedure_view") + + +class ProcedureDelete(RevisionMixin, LoginRequiredMixin, DeleteView): + model = Procedure + template_name = "confirm_delete.html" + + class PathologicalProcessView(LoginRequiredMixin, UserConfigurablePaginationMixin, SingleTableMixin, FilterView): model = PathologicalProcess table_class = PathologicalProcessTable @@ -3137,7 +3197,7 @@ def categories_list(request): results = None if query: - from .models import Condition, Finding, Structure, Subspecialty, Presentation + from .models import Condition, Finding, Structure, Subspecialty, Presentation, Procedure # Helper to conditionally populate each result list only when the # corresponding type is requested (or when no types filter provided). @@ -3150,6 +3210,7 @@ def categories_list(request): "structures": Structure.objects.filter(name__icontains=query).order_by("name") if include("structures") else (), "subspecialties": Subspecialty.objects.filter(name__icontains=query).order_by("name") if include("subspecialties") else (), "presentations": Presentation.objects.filter(name__icontains=query).order_by("name") if include("presentations") else (), + "procedures": Procedure.objects.filter(name__icontains=query).order_by("name") if include("procedures") else (), } return render(request, "atlas/categories_list.html", {"query": query, "results": results}) @@ -3163,9 +3224,9 @@ def categories_search_partial(request): """ query = request.GET.get("q", "").strip() types = request.GET.getlist("types") - results = {"conditions": (), "findings": (), "structures": (), "subspecialties": (), "presentations": ()} + results = {"conditions": (), "findings": (), "structures": (), "subspecialties": (), "presentations": (), "procedures": ()} if query: - from .models import Condition, Finding, Structure, Subspecialty, Presentation + from .models import Condition, Finding, Structure, Subspecialty, Presentation, Procedure def include(name: str) -> bool: return not types or name in types @@ -3176,6 +3237,7 @@ def categories_search_partial(request): "structures": Structure.objects.filter(name__icontains=query).order_by("name") if include("structures") else (), "subspecialties": Subspecialty.objects.filter(name__icontains=query).order_by("name") if include("subspecialties") else (), "presentations": Presentation.objects.filter(name__icontains=query).order_by("name") if include("presentations") else (), + "procedures": Procedure.objects.filter(name__icontains=query).order_by("name") if include("procedures") else (), } return render(request, "atlas/_categories_search_results.html", {"query": query, "results": results})