"""Request/response schemas for templates (模板管理).""" from datetime import datetime from decimal import Decimal from pydantic import BaseModel, ConfigDict, Field from app.models.template import Template from app.models.template_field import TemplateField from app.schemas.common import TypographyMixin class TemplateFieldInput(BaseModel): """One placement of a library field inside a template. The client sends the whole ordered selection on create and on update; the server replaces the stored rows with it. ``sort`` is a plain ascending integer and is the *only* thing that decides render order. """ field_id: int sort: int = Field(default=0, ge=-1_000_000, le=1_000_000) class TemplateFieldRead(TypographyMixin, BaseModel): """A template's field, flattened with the library row it points at. Persistence keeps the two apart — a placement row plus the library row it references — but a client rendering an outline wants one flat record, so the typography is denormalised into the response. That is also what keeps the outline in sync: there is only ever one copy of a field's name and styling, in the library. """ id: int field_id: int sort: int name: str level: int font_size: Decimal font_color: str @classmethod def from_model(cls, item: TemplateField) -> "TemplateFieldRead": """Flatten a placement row and its library row into one record.""" return cls( id=item.id, field_id=item.field_id, sort=item.sort, name=item.field.name, level=item.field.level, font_size=item.field.font_size, font_color=item.field.font_color, ) class TemplateBase(BaseModel): """Shared body of the create/update payloads.""" name: str = Field(min_length=1, max_length=255) abstract: str | None = None class TemplateCreate(TemplateBase): """Payload for ``POST /templates``. The field selection is free: any subset, any order, repeats allowed. The server stores it as given and orders it by ``sort`` on read. """ fields: list[TemplateFieldInput] = Field(default_factory=list) class TemplateUpdate(BaseModel): """Payload for ``PATCH /templates/{id}``. ``fields`` is a full replacement when present — omit it to leave the selection untouched. """ name: str | None = Field(default=None, min_length=1, max_length=255) abstract: str | None = None fields: list[TemplateFieldInput] | None = None class TemplateListItem(BaseModel): """A template as it appears in the list table — no field rows.""" model_config = ConfigDict(from_attributes=True) id: int name: str abstract: str | None field_count: int created_at: datetime updated_at: datetime class TemplateRead(BaseModel): """A template with its outline, already ordered by ``sort``.""" model_config = ConfigDict(from_attributes=True) id: int name: str abstract: str | None #: The outline, ordered by ``sort`` — the ORM relationship sets that #: ordering, so this list is display-ready with no client-side sorting. fields: list[TemplateFieldRead] created_at: datetime updated_at: datetime @classmethod def from_model(cls, template: Template) -> "TemplateRead": """Build the response from a template and its placement rows.""" return cls( id=template.id, name=template.name, abstract=template.abstract, fields=[TemplateFieldRead.from_model(item) for item in template.items], created_at=template.created_at, updated_at=template.updated_at, )