Refactor NormalCase model to use age_days instead of age_years, update forms, filters, and templates accordingly for improved age handling
This commit is contained in:
+80
-8
@@ -615,10 +615,10 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
class NormalCase(models.Model):
|
||||
"""Marks a Case as a 'normal' example used in the Atlas normals section.
|
||||
|
||||
We store a nearest-year age (age_years) and optional primary
|
||||
examination/modality to make filtering simple and fast. A OneToOne
|
||||
relation keeps normal-specific fields separate from the main Case
|
||||
object and is non-destructive.
|
||||
This version stores a canonical `age_days` integer (patient age in days).
|
||||
We keep the examination/modality metadata and authored fields. The
|
||||
age fields previously stored (`age_years`, `age_value`, `age_unit`) have
|
||||
been removed in favour of the single canonical `age_days` value.
|
||||
"""
|
||||
case = models.OneToOneField(
|
||||
Case,
|
||||
@@ -627,13 +627,26 @@ class NormalCase(models.Model):
|
||||
help_text="The Case that is marked as a normal example.",
|
||||
)
|
||||
|
||||
age_years = models.PositiveSmallIntegerField(
|
||||
# Canonical internal representation: patient age in days
|
||||
age_days = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(0), MaxValueValidator(150)],
|
||||
help_text="Age of the patient at scan in years (rounded to nearest year).",
|
||||
help_text="Patient age at scan in days (canonical).",
|
||||
)
|
||||
|
||||
# Helpful constants for converting/displaying ages. These are not
|
||||
# stored directly on the model but are used by forms/views/templates.
|
||||
AGE_UNIT_YEARS = "years"
|
||||
AGE_UNIT_MONTHS = "months"
|
||||
AGE_UNIT_WEEKS = "weeks"
|
||||
AGE_UNIT_DAYS = "days"
|
||||
AGE_UNIT_CHOICES = [
|
||||
(AGE_UNIT_YEARS, "years"),
|
||||
(AGE_UNIT_MONTHS, "months"),
|
||||
(AGE_UNIT_WEEKS, "weeks"),
|
||||
(AGE_UNIT_DAYS, "days"),
|
||||
]
|
||||
|
||||
examination = models.ForeignKey(
|
||||
Examination,
|
||||
null=True,
|
||||
@@ -668,11 +681,70 @@ class NormalCase(models.Model):
|
||||
ordering = ["-added_date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Normal: {self.case} ({self.age_years or 'unknown'}y)"
|
||||
return f"Normal: {self.case} ({self.display_age()})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.case.get_absolute_url()
|
||||
|
||||
@staticmethod
|
||||
def to_days(value: int | float, unit: str) -> int | None:
|
||||
"""Convert a numeric value + unit into integer days (rounded).
|
||||
|
||||
Examples:
|
||||
to_days(2, 'years') -> ~730
|
||||
to_days(6, 'months') -> ~183
|
||||
"""
|
||||
try:
|
||||
if value is None or unit is None:
|
||||
return None
|
||||
v = float(value)
|
||||
if unit == NormalCase.AGE_UNIT_YEARS:
|
||||
return int(round(v * 365.25))
|
||||
if unit == NormalCase.AGE_UNIT_MONTHS:
|
||||
return int(round(v * 30.4375))
|
||||
if unit == NormalCase.AGE_UNIT_WEEKS:
|
||||
return int(round(v * 7))
|
||||
if unit == NormalCase.AGE_UNIT_DAYS:
|
||||
return int(round(v))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def days_to_value_unit(days: int | None) -> tuple[int | None, str | None]:
|
||||
"""Convert canonical days into a (value, unit) tuple for display.
|
||||
|
||||
Prefer years for ages >= 365 days, months for ages >= 30 days,
|
||||
weeks for >=7 days, else days.
|
||||
"""
|
||||
if days is None:
|
||||
return (None, None)
|
||||
try:
|
||||
d = int(days)
|
||||
if d >= 365:
|
||||
yrs = int(round(d / 365.25))
|
||||
return (yrs, NormalCase.AGE_UNIT_YEARS)
|
||||
if d >= 30:
|
||||
months = int(round(d / 30.4375))
|
||||
return (months, NormalCase.AGE_UNIT_MONTHS)
|
||||
if d >= 7:
|
||||
weeks = int(round(d / 7))
|
||||
return (weeks, NormalCase.AGE_UNIT_WEEKS)
|
||||
return (d, NormalCase.AGE_UNIT_DAYS)
|
||||
except Exception:
|
||||
return (None, None)
|
||||
|
||||
def display_age(self) -> str:
|
||||
"""Return a human friendly age string derived from age_days."""
|
||||
if self.age_days is None:
|
||||
return "age unknown"
|
||||
val, unit = NormalCase.days_to_value_unit(self.age_days)
|
||||
if val is None or unit is None:
|
||||
return "age unknown"
|
||||
# abbreviate years to 'y' for backwards compatibility where helpful
|
||||
if unit == NormalCase.AGE_UNIT_YEARS:
|
||||
return f"{val}y"
|
||||
return f"{val} {unit}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# If examination/modality not specified, try to infer from the first series
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user