Files
AdventureLog/backend/server/adventures/utils/datetime_utils.py
Sean Morley fe30ad90e6 Implement timezone-aware date handling and normalization for visits
- Added utility functions to ensure datetime values are UTC-aware, improving consistency in date handling across the application.
- Introduced migrations to update existing visit records, making start and end dates timezone-aware.
- Normalized all-day visit end dates to UTC midnight, ensuring accurate representation of all-day events.
- Updated serializers and services to utilize new datetime utilities, enhancing the handling of visit dates in various contexts.
- Added comprehensive tests for new datetime utilities to ensure correct functionality and edge case handling.
2026-07-08 17:11:56 -04:00

73 lines
2.2 KiB
Python

from __future__ import annotations
from datetime import date, datetime, time, timezone as dt_timezone
from django.utils import timezone
def ensure_aware_utc(value: datetime | None) -> datetime | None:
"""Return a UTC-aware datetime, treating naive values as UTC."""
if value is None:
return None
if timezone.is_naive(value):
return timezone.make_aware(value, dt_timezone.utc)
return value
def aware_utc_from_date(value: date, *, end_of_day: bool = False) -> datetime:
"""Build a UTC-aware datetime at the start or end of a calendar day."""
combined = datetime.combine(value, time.max if end_of_day else time.min)
return timezone.make_aware(combined, dt_timezone.utc)
def aware_utc_from_date_and_time(value: date, time_value: time) -> datetime:
"""Build a UTC-aware datetime from a date and time."""
combined = datetime.combine(value, time_value)
return timezone.make_aware(combined, dt_timezone.utc)
def is_midnight_utc(value: datetime | None) -> bool:
if value is None:
return False
utc_value = ensure_aware_utc(value).astimezone(dt_timezone.utc)
return utc_value.time() == time.min
def is_end_of_day_utc(value: datetime | None) -> bool:
if value is None:
return False
utc_value = ensure_aware_utc(value).astimezone(dt_timezone.utc)
return utc_value.time() == time.max
def is_all_day_bound(value: datetime | None) -> bool:
"""True when a datetime is stored as UTC midnight or legacy end-of-day."""
return is_midnight_utc(value) or is_end_of_day_utc(value)
def normalize_all_day_visit_dates(
start: datetime | None,
end: datetime | None,
) -> tuple[datetime | None, datetime | None]:
"""
Normalize all-day visits to UTC midnight on each calendar date.
All-day visits always use T00:00:00Z for both start_date and end_date.
"""
start = ensure_aware_utc(start)
end = ensure_aware_utc(end)
if start is None:
return start, end
if not is_midnight_utc(start):
return start, end
if end is None:
return start, aware_utc_from_date(start.date())
if is_all_day_bound(end):
return start, aware_utc_from_date(end.date())
return start, end