Files
F-r-ldraledigplannerare/backend/app/routers/leave_wishes.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

112 lines
4.6 KiB
Python

"""HTTP routes for leave wishes.
Routers stay thin: validate via Pydantic, call the service, map domain errors
to HTTP. Business logic (recurrence/parent_mode/goal_override validation,
month-day range checks) lives in LeaveWishService. See PLANNING_ENGINE_SPEC.md §4.
"""
from uuid import UUID
from fastapi import APIRouter, HTTPException, status
from app.dependencies import LeaveWishServiceDep
from app.models.leave_wish import (
LeaveWishCreate,
LeaveWishResponse,
LeaveWishUpdate,
)
from app.utils.errors import NotFoundError, ValidationError
router = APIRouter(prefix="/api/leave-wishes", tags=["leave-wishes"])
@router.post("", response_model=LeaveWishResponse, status_code=status.HTTP_201_CREATED)
async def create_leave_wish(
payload: LeaveWishCreate, service: LeaveWishServiceDep
) -> LeaveWishResponse:
try:
entity = await service.create(
workspace_id=payload.workspace_id,
child_id=payload.child_id,
name=payload.name,
name_message_key=payload.name_message_key,
window_start_month=payload.window_start_month,
window_start_day=payload.window_start_day,
window_end_month=payload.window_end_month,
window_end_day=payload.window_end_day,
target_weeks=payload.target_weeks,
recurrence=payload.recurrence,
first_year=payload.first_year,
last_year=payload.last_year,
priority=payload.priority,
parent_mode=payload.parent_mode,
parent_personal_data_uuid=payload.parent_personal_data_uuid,
goal_override=payload.goal_override,
window_mode=payload.window_mode,
)
except ValidationError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
return LeaveWishResponse.from_entity(entity)
@router.get("/{workspace_id}", response_model=list[LeaveWishResponse])
async def list_leave_wishes(
workspace_id: UUID, service: LeaveWishServiceDep
) -> list[LeaveWishResponse]:
"""List all leave wishes for a workspace. Matches PLANNING_ENGINE_SPEC.md §4
(`GET /api/leave-wishes/{workspace_id}` — list-by-workspace, not get-by-id)."""
entities = await service.list_for_workspace(workspace_id)
return [LeaveWishResponse.from_entity(e) for e in entities]
@router.put("/{leave_wish_id}", response_model=LeaveWishResponse)
async def update_leave_wish(
leave_wish_id: UUID,
payload: LeaveWishUpdate,
service: LeaveWishServiceDep,
) -> LeaveWishResponse:
fields_set = payload.model_fields_set
try:
entity = await service.update(
leave_wish_id=leave_wish_id,
child_id=payload.child_id,
name=payload.name,
name_message_key=payload.name_message_key,
window_start_month=payload.window_start_month,
window_start_day=payload.window_start_day,
window_end_month=payload.window_end_month,
window_end_day=payload.window_end_day,
target_weeks=payload.target_weeks,
recurrence=payload.recurrence,
first_year=payload.first_year,
last_year=payload.last_year,
priority=payload.priority,
parent_mode=payload.parent_mode,
parent_personal_data_uuid=payload.parent_personal_data_uuid,
goal_override=payload.goal_override,
window_mode=payload.window_mode,
_clear_child_id="child_id" in fields_set and payload.child_id is None,
_clear_last_year="last_year" in fields_set and payload.last_year is None,
_clear_parent_personal_data_uuid=(
"parent_personal_data_uuid" in fields_set
and payload.parent_personal_data_uuid is None
),
_clear_goal_override=("goal_override" in fields_set and payload.goal_override is None),
_clear_name_message_key=(
"name_message_key" in fields_set and payload.name_message_key is None
),
)
except ValidationError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
except NotFoundError as exc:
raise HTTPException(status.HTTP_404_NOT_FOUND, str(exc)) from exc
return LeaveWishResponse.from_entity(entity)
@router.delete("/{leave_wish_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_leave_wish(leave_wish_id: UUID, service: LeaveWishServiceDep) -> None:
try:
await service.delete(leave_wish_id)
except NotFoundError as exc:
raise HTTPException(status.HTTP_404_NOT_FOUND, str(exc)) from exc