From 00680c8b9eb14f436f54b458298974037d303851 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 2 Feb 2026 11:26:11 +0000 Subject: [PATCH] Enhance NormalCaseFilter to support flexible age filtering with unit selection; update normals_list template with detailed help text for improved user guidance --- atlas/filters.py | 77 ++++++++++++++++++++----- atlas/templates/atlas/normals_list.html | 11 ++++ templates/base.html | 1 + 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/atlas/filters.py b/atlas/filters.py index 7fc6bd54..47857abe 100755 --- a/atlas/filters.py +++ b/atlas/filters.py @@ -1,6 +1,7 @@ import django_filters +from django import forms from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Div +from crispy_forms.layout import Layout, Div, Field from .models import ( Case, @@ -355,8 +356,16 @@ class NormalCaseFilter(django_filters.FilterSet): modality = django_filters.ModelChoiceFilter(queryset=Series.objects.none()) # Accept min/max as years in the UI but filter against the canonical # `age_days` field by converting years -> days. - min_age = django_filters.NumberFilter(label="Min age", method="filter_min_age") - max_age = django_filters.NumberFilter(label="Max age", method="filter_max_age") + # Numeric min/max values entered by the user (interpreted via `age_unit`) + min_value = django_filters.NumberFilter(label="Min", method="filter_min_value") + max_value = django_filters.NumberFilter(label="Max", method="filter_max_value") + AGE_UNIT_CHOICES = ( + ("days", "Days"), + ("weeks", "Weeks"), + ("months", "Months"), + ("years", "Years"), + ) + age_unit = django_filters.ChoiceFilter(label="Unit", choices=AGE_UNIT_CHOICES) class Meta: model = NormalCase @@ -406,30 +415,70 @@ class NormalCaseFilter(django_filters.FilterSet): helper.form_class = "row g-2 align-items-end" helper.layout = Layout( Div( - Div("examination", css_class="col-md-4"), - Div("modality", css_class="col-md-4"), - Div("age_days", css_class="col-md-2"), - Div("min_age", css_class="col-md-2"), - Div("max_age", css_class="col-md-2"), + Div("examination", css_class="col-md-3"), + Div("modality", css_class="col-md-3"), + Div( + Field("min_value", wrapper_class="mb-0"), + Field("max_value", wrapper_class="mb-0"), + Field("age_unit", wrapper_class="mb-0"), + css_class="col-md-6 d-flex gap-2 align-items-stretch border rounded p-2 bg-body-tertiary", + ), css_class="row g-2 align-items-end", ) ) - # Assign helper to the generated form instance + # Assign helper to the generated form instance and tweak widgets if hasattr(self, 'form') and self.form is not None: - self.form.helper = helper + # Wider numeric inputs and default unit + try: + form = self.form + if "min_value" in form.fields: + form.fields["min_value"].widget = forms.NumberInput(attrs={"class": "form-control", "min": "0", "step": "1", "style": "width:140px;"}) + if "max_value" in form.fields: + form.fields["max_value"].widget = forms.NumberInput(attrs={"class": "form-control", "min": "0", "step": "1", "style": "width:140px;"}) + if "age_unit" in form.fields: + form.fields["age_unit"].initial = "days" + form.fields["age_unit"].widget.attrs.update({"class": "form-select", "style": "width:220px;"}) + form.helper = helper + except Exception: + # don't crash if form structure differs + self.form.helper = helper except Exception: pass - def filter_min_age(self, queryset, name, value): + def _convert_to_days(self, value, unit: str) -> int: try: - days = int(float(value) * 365.25) + v = float(value) + except Exception: + raise + + if unit == "days": + return int(v) + if unit == "weeks": + return int(v * 7) + if unit == "months": + # approximate month length + return int(v * (365.25 / 12.0)) + # default: years + return int(v * 365.25) + + def filter_min_value(self, queryset, name, value): + try: + unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "years") + except Exception: + unit = "years" + try: + days = self._convert_to_days(value, unit) except Exception: return queryset return queryset.filter(age_days__gte=days) - def filter_max_age(self, queryset, name, value): + def filter_max_value(self, queryset, name, value): try: - days = int(float(value) * 365.25) + unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "years") + except Exception: + unit = "years" + try: + days = self._convert_to_days(value, unit) except Exception: return queryset return queryset.filter(age_days__lte=days) diff --git a/atlas/templates/atlas/normals_list.html b/atlas/templates/atlas/normals_list.html index ed51f5f7..b00c4097 100644 --- a/atlas/templates/atlas/normals_list.html +++ b/atlas/templates/atlas/normals_list.html @@ -1,10 +1,19 @@ {% extends "atlas/base.html" %} {% load static %} {% load crispy_forms_tags %} +{% load help_tags %} {% block content %}

Normals

+ {% help "Help" %} +

This page lists all normal cases available in the atlas.

+

You can use the filter form to narrow down the list based on criteria such as age, modality, or examination type.

+

Click on a case title to view the case.

+ +

See a case that isn't normal? Please add feedback to the case so it can be reviewed.

+ + {% endhelp%}
@@ -54,3 +63,5 @@
{% endblock %} + +{% block css %}{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index bf56f197..c91bf092 100644 --- a/templates/base.html +++ b/templates/base.html @@ -181,6 +181,7 @@ } + {% block css %} {% endblock %}