from datetime import timedelta from typing import TYPE_CHECKING from sqlalchemy import Interval from sqlmodel import ( Session, Relationship, Field, ) from . import mixin from .base import ( BaseSQLModel, DocumentedStrEnum, auto_enum, ) if TYPE_CHECKING: from .route import Route, RoutePart # region # Place ############################################################### class PlaceType(DocumentedStrEnum): START = auto_enum() # Only check in (make GPS available) (TODO: determine based on route) PLACE = auto_enum() # Check in / Check out (subtract time between in&out) TAG = auto_enum() # Instant in&out at the same time FINISH = auto_enum() # Only check out (and disable GPS) (TODO: determine based on route) # ############################################################################## class VisitedCountType(DocumentedStrEnum): NONE = auto_enum() ONE_VISIT = auto_enum() EACH_VISIT = auto_enum() # ############################################################################## class PlaceBase( mixin.Name, mixin.ShortName, mixin.Contact, mixin.Description, mixin.LocationWithRadius, mixin.QuestionInfo, BaseSQLModel, ): place_type: PlaceType = Field( default=PlaceType.PLACE, nullable=False, ) max_points: int | None = Field( default=None, ge=0, description="Max points for this place, None will disable scoring at this place", ) visited_points: int | None = Field( default=None, ge=0, description="Visited points for this place, None will disable this function", ) visited_count_type: VisitedCountType = Field( default=VisitedCountType.NONE, ) skipped_penalty_points: int | None = Field( default=None, ge=0, description="Skipped penalty for this place, amount will be subtracted, None will disable this function", ) place_time: timedelta | None = Field( default=None, nullable=True, description="Time for question at this place during testing.", sa_type=Interval, ) # Properties to receive via API on creation class PlaceCreate(PlaceBase): pass # Properties to receive via API on update, all are optional class PlaceUpdate( PlaceBase, mixin.ShortNameUpdate, ): place_type: PlaceType | None = Field(default=None) # type: ignore visited_count_type: VisitedCountType | None = Field(default=None) # type: ignore class Place(mixin.RowId, PlaceBase, table=True): # --- database only items -------------------------------------------------- # --- read only items ------------------------------------------------------ # --- back_populates links ------------------------------------------------- routes: list["Route"] = Relationship( sa_relationship_kwargs={ "primaryjoin": "or_(" "Place.id==Route.start_id," "Place.id==Route.finish_id," "Place.id==RoutePart.place_id," "Place.id==RoutePart.to_place_id," ")", "foreign_keys": "[" "Route.start_id," "Route.finish_id," "RoutePart.place_id," "RoutePart.to_place_id" "]", "viewonly": True, }, ) next_places: list["Place"] = Relationship( back_populates="previous_place", sa_relationship_kwargs={ "secondary": "route_part", "primaryjoin": "Place.id == RoutePart.place_id", "secondaryjoin": "Place.id == RoutePart.to_place_id", "viewonly": True, }, ) previous_place: list["Place"] = Relationship( back_populates="next_places", sa_relationship_kwargs={ "secondary": "route_part", "primaryjoin": "Place.id == RoutePart.to_place_id", "secondaryjoin": "Place.id == RoutePart.place_id", "viewonly": True, }, ) route_parts: list["RoutePart"] = Relationship(back_populates="place", sa_relationship_kwargs={"foreign_keys": "[RoutePart.place_id]"}) # --- CRUD actions --------------------------------------------------------- @classmethod def create(cls, *, session: Session, create_obj: PlaceCreate) -> "Place": data_obj = create_obj.model_dump(exclude_unset=True) db_obj = cls.model_validate(data_obj) session.add(db_obj) session.commit() session.refresh(db_obj) return db_obj @classmethod def update( cls, *, session: Session, db_obj: "Place", in_obj: PlaceUpdate ) -> "Place": data_obj = in_obj.model_dump(exclude_unset=True) db_obj.sqlmodel_update(data_obj) session.add(db_obj) session.commit() session.refresh(db_obj) return db_obj # Properties to return via API, id is always required class PlacePublic(mixin.RowIdPublic, PlaceBase): pass class PlacesPublic(BaseSQLModel): data: list[PlacePublic] count: int # endregion