Full CRUD on Participant scoped to the caller's organization via new get_route_in_organization/get_participant_in_organization helpers in app/dependencies.py (shared cross-cutting spot, reused by task 6). PATCH is_active is the primary deactivation path — replaces the old spreadsheet-era "empty order" trick and preserves order history for participants who leave and later return. DELETE is a separate, superadmin-gated hard-delete for GDPR erasure requests, which cascades to the participant's orders. Adds UNIQUE(route_id, email) at the DB level (existing 308 participants already conform, per task 3's dedup) plus email normalization on create/update, so case-variant duplicates can't reappear via the API. Every write path relies on catching the constraint's IntegrityError for a clean 409 rather than a racy pre-check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
from typing import TYPE_CHECKING, List, Optional
|
|
|
|
from pydantic import field_validator
|
|
from sqlalchemy import UniqueConstraint
|
|
from sqlmodel import Field, Relationship, SQLModel
|
|
|
|
if TYPE_CHECKING:
|
|
from app.models.order import Order
|
|
from app.models.route import Route
|
|
|
|
|
|
def _normalize_email(value: str) -> str:
|
|
return value.strip().lower()
|
|
|
|
|
|
class ParticipantBase(SQLModel):
|
|
name: str
|
|
email: str
|
|
phone: Optional[str] = Field(default=None)
|
|
is_active: bool = Field(default=True)
|
|
|
|
@field_validator("email")
|
|
@classmethod
|
|
def _validate_email(cls, v: str) -> str:
|
|
return _normalize_email(v)
|
|
|
|
|
|
class Participant(ParticipantBase, table=True):
|
|
__tablename__ = "participant"
|
|
__table_args__ = (UniqueConstraint("route_id", "email", name="uq_participant_route_email"),)
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
route_id: int = Field(foreign_key="route.id", ondelete="CASCADE")
|
|
|
|
route: "Route" = Relationship(back_populates="participants")
|
|
orders: List["Order"] = Relationship(
|
|
back_populates="participant", sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
|
)
|
|
|
|
|
|
class ParticipantCreate(ParticipantBase):
|
|
route_id: int
|
|
phone: str # påkrævet ved oprettelse, selvom det er Optional i basen/DB'en
|
|
|
|
|
|
class ParticipantUpdate(SQLModel):
|
|
name: Optional[str] = None
|
|
email: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
@field_validator("email")
|
|
@classmethod
|
|
def _validate_email(cls, v: Optional[str]) -> Optional[str]:
|
|
return _normalize_email(v) if v is not None else v
|
|
|
|
|
|
class ParticipantPublic(ParticipantBase):
|
|
id: int
|
|
route_id: int
|