Enhance NormalCaseFilter to support flexible age filtering with unit selection; update normals_list template with detailed help text for improved user guidance
This commit is contained in:
+62
-13
@@ -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:
|
||||
# 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)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
{% extends "atlas/base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load help_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Normals</h1>
|
||||
{% help "Help" %}
|
||||
<p>This page lists all normal cases available in the atlas.</p>
|
||||
<p>You can use the filter form to narrow down the list based on criteria such as age, modality, or examination type.</p>
|
||||
<p>Click on a case title to view the case.</p>
|
||||
|
||||
<p>See a case that isn't normal? Please add feedback to the case so it can be reviewed.</p>
|
||||
|
||||
{% endhelp%}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
@@ -54,3 +63,5 @@
|
||||
</nav>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
@@ -181,6 +181,7 @@
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user