fix(photos): accept bare-date filter bounds, send T00:00:00 from UI

GET /api/v1/photos rejected ?date_from=2026-04-10 with 422 because
pydantic v2's datetime parser doesn't accept date-only strings. The
frontend has been padding date_to with T23:59:59 forever to make the
upper bound inclusive, but date_from went out as a bare YYYY-MM-DD,
so every date-range filter request 422'd and the grid showed nothing.

Frontend: pad date_from with T00:00:00 the same way date_to gets
T23:59:59 — symmetry, and pydantic v2 accepts the full form.

Backend: change date_from/date_to to Optional[str] and parse with
datetime.fromisoformat in the handler. fromisoformat accepts both
bare dates ('2026-04-10' -> midnight) and full ISO strings, so any
older client that still sends a date-only value continues to work.
Tz-aware values get coerced to naive UTC before binding (matches the
taken_at column's  shape and the same
fix applied to PATCH /photos/{id} earlier today). Bad input returns
400 with a clear message instead of pydantic's 422.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudio
2026-05-10 23:16:09 +02:00
parent 7b153f0d28
commit d580796dc8
2 changed files with 39 additions and 12 deletions

View File

@@ -52,8 +52,12 @@ router = APIRouter()
@router.get("")
async def list_photos(
q: Optional[str] = None,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
# Accept either a bare date ("2026-04-10") or a full ISO datetime
# ("2026-04-10T23:59:59"). pydantic v2's datetime parser rejects
# the bare form with 422; we coerce manually below so older
# clients that send a date-only string keep working.
date_from: Optional[str] = None,
date_to: Optional[str] = None,
folder_id: Optional[str] = None,
tag_ids: Optional[str] = None,
media_type: Optional[str] = None,
@@ -129,11 +133,33 @@ async def list_photos(
)
)
# Date range
if date_from:
filters.append(Photo.taken_at >= date_from)
if date_to:
filters.append(Photo.taken_at <= date_to)
# Date range. Both inputs are strings to keep pydantic from rejecting
# bare-date forms ("2026-04-10") with 422; coerce here. fromisoformat
# accepts both bare dates and full ISO datetimes — when given a date,
# it returns midnight, which is what we want for the lower bound.
def _parse_bound(s: Optional[str]) -> Optional[datetime]:
if not s:
return None
try:
dt = datetime.fromisoformat(s)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Invalid date: {s!r} (expected YYYY-MM-DD or ISO 8601)",
)
# If the caller sent a tz-aware string, normalize to naive UTC —
# the photos.taken_at column is `timestamp without time zone`.
if dt.tzinfo is not None:
from datetime import timezone
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
return dt
parsed_date_from = _parse_bound(date_from)
parsed_date_to = _parse_bound(date_to)
if parsed_date_from:
filters.append(Photo.taken_at >= parsed_date_from)
if parsed_date_to:
filters.append(Photo.taken_at <= parsed_date_to)
# Folder filter. The sidebar can pass either a SourceRoot id or a
# Folder id; both should include descendants so clicking a parent