Files
F-r-ldraledigplannerare/backend/app/models/leave_wish.py
T
jacobh2 dc4c0a5edd Show preset wish names in the current UI language
Preset wishes were created with `name: t(preset.labelKey)`, so the
translated string was persisted as the wish's name. A wish created while
the UI was Swedish stayed "Jul" in English forever -- the Swedish text
was the data, so no display-layer change could fix it.

Wish names are user-editable free text, so `name` cannot simply be
translated at render time. Migration 007 adds a nullable
`leave_wish.name_message_key` recording where a name came from: preset
wishes store the i18n key alongside the translated literal, and the UI
renders the key when present.

The key is cleared server-side the moment `name` actually changes,
compared against the stored value rather than trusting the caller, so no
client can forget to clear it and silently revert a user's own name to a
preset label.

An unknown key (a stale row, a renamed key) falls back to the stored
literal via an isTranslationKey guard, so a bad key can never crash the
page or leak a raw key to the user. Rows predating this column keep
displaying their stored name.

_FULL_SCHEMA in db/connection.py is updated in step with the migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
2026-09-14 22:32:54 +02:00

131 lines
4.6 KiB
Python

"""API schemas (Pydantic) for LeaveWish.
Schemas validate request bodies and shape responses. Separate from domain entities
so the wire format can evolve independently. Business-rule validation (e.g. the
parent_mode / parent_personal_data_uuid pairing) lives in the service, not here —
these schemas only enforce shape and range.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from app.entities.leave_wish import LeaveWish
class LeaveWishCreate(BaseModel):
workspace_id: UUID
child_id: UUID | None = None
name: str = Field(min_length=1, max_length=255)
# i18n message key when `name` came from a preset (e.g.
# "wishEditor.presetSommar") — see app/entities/leave_wish.py. Stored
# alongside the translated `name` so the frontend can re-translate it;
# not validated against a key catalogue here (that's a frontend concern).
name_message_key: str | None = Field(default=None, max_length=255)
window_start_month: int = Field(ge=1, le=12)
window_start_day: int = Field(ge=1, le=31)
window_end_month: int = Field(ge=1, le=12)
window_end_day: int = Field(ge=1, le=31)
target_weeks: int = Field(ge=1, le=52)
recurrence: str = "once" # 'yearly' | 'once'
first_year: int = Field(ge=2000, le=2200)
last_year: int | None = Field(default=None, ge=2000, le=2200)
priority: int = Field(default=1, ge=1)
parent_mode: str = "either" # 'either' | 'both' | 'specific'
parent_personal_data_uuid: UUID | None = None
goal_override: str | None = None # 'max_time' | 'max_money' | 'balanced' | None
# PLANNING_ENGINE_SPEC.md §10.1 — 'fixed': window_start_*/window_end_* are the
# exact placement (default). 'flexible': the same fields describe the
# bounding range the allocator searches within for the best-costing window.
window_mode: str = "fixed" # 'fixed' | 'flexible'
class LeaveWishUpdate(BaseModel):
"""Partial update — all fields optional; only provided fields are changed."""
child_id: UUID | None = None
name: str | None = Field(None, min_length=1, max_length=255)
# Present + null in the payload means "clear it"; absent means "leave
# unchanged" (see `model_fields_set` handling in the router). The service
# also clears this automatically whenever `name` actually changes, so a
# caller can never forget to and leave a stale key pointing at old text.
name_message_key: str | None = Field(default=None, max_length=255)
window_start_month: int | None = Field(None, ge=1, le=12)
window_start_day: int | None = Field(None, ge=1, le=31)
window_end_month: int | None = Field(None, ge=1, le=12)
window_end_day: int | None = Field(None, ge=1, le=31)
target_weeks: int | None = Field(None, ge=1, le=52)
recurrence: str | None = None
first_year: int | None = Field(None, ge=2000, le=2200)
last_year: int | None = Field(None, ge=2000, le=2200)
priority: int | None = Field(None, ge=1)
parent_mode: str | None = None
parent_personal_data_uuid: UUID | None = None
goal_override: str | None = None
window_mode: str | None = None # 'fixed' | 'flexible'
class LeaveWishResponse(BaseModel):
id: UUID
workspace_id: UUID
child_id: UUID | None
name: str
name_message_key: str | None
window_start_month: int
window_start_day: int
window_end_month: int
window_end_day: int
target_weeks: int
recurrence: str
first_year: int
last_year: int | None
priority: int
parent_mode: str
parent_personal_data_uuid: UUID | None
goal_override: str | None
window_mode: str
created_at: datetime | None
updated_at: datetime | None
@classmethod
def from_entity(cls, entity: LeaveWish) -> "LeaveWishResponse":
return cls(
id=entity.id,
workspace_id=entity.workspace_id,
child_id=entity.child_id,
name=entity.name,
name_message_key=entity.name_message_key,
window_start_month=entity.window_start_month,
window_start_day=entity.window_start_day,
window_end_month=entity.window_end_month,
window_end_day=entity.window_end_day,
target_weeks=entity.target_weeks,
recurrence=entity.recurrence,
first_year=entity.first_year,
last_year=entity.last_year,
priority=entity.priority,
parent_mode=entity.parent_mode,
parent_personal_data_uuid=entity.parent_personal_data_uuid,
goal_override=entity.goal_override,
window_mode=entity.window_mode,
created_at=entity.created_at,
updated_at=entity.updated_at,
)