720 lines
21 KiB
Python
720 lines
21 KiB
Python
import re
|
|
from datetime import UTC, date, datetime
|
|
from enum import Enum
|
|
from typing import Annotated
|
|
|
|
from pydantic import BaseModel, StringConstraints, field_validator
|
|
from sqlalchemy import Index, MetaData, event
|
|
from sqlalchemy.orm import Session, object_session
|
|
from sqlmodel import Field, Relationship, SQLModel
|
|
|
|
from ..config import settings
|
|
from ..utils.utils import remove_attachment, remove_backup, remove_image
|
|
|
|
convention = {
|
|
"ix": "ix_%(column_0_label)s",
|
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
"pk": "pk_%(table_name)s",
|
|
}
|
|
|
|
SQLModel.metadata = MetaData(naming_convention=convention)
|
|
|
|
|
|
@event.listens_for(Session, "after_commit")
|
|
def cleanup_after_commit(session):
|
|
if hasattr(session, "_images_to_delete"):
|
|
for filename in session._images_to_delete:
|
|
remove_image(filename)
|
|
delattr(session, "_images_to_delete")
|
|
|
|
if hasattr(session, "_attachments_to_delete"):
|
|
for attachment in session._attachments_to_delete:
|
|
remove_attachment(attachment.trip_id, attachment.stored_filename)
|
|
delattr(session, "_attachments_to_delete")
|
|
|
|
if hasattr(session, "_backups_to_delete"):
|
|
for filename in session._backups_to_delete:
|
|
remove_backup(filename)
|
|
delattr(session, "_backups_to_delete")
|
|
|
|
|
|
def _prefix_assets_url(filename: str) -> str:
|
|
base = settings.ASSETS_URL
|
|
if not base.endswith("/"):
|
|
base += "/"
|
|
return base + filename
|
|
|
|
|
|
class AuthParams(BaseModel):
|
|
oidc: str | None
|
|
register_enabled: bool
|
|
|
|
|
|
class TripItemStatusEnum(str, Enum):
|
|
PENDING = "pending"
|
|
CONFIRMED = "booked"
|
|
CONSTRAINT = "constraint"
|
|
OPTIONAL = "optional"
|
|
|
|
|
|
class PackingListCategoryEnum(str, Enum):
|
|
CLOTHES = "clothes"
|
|
TOILETRIES = "toiletries"
|
|
TECH = "tech"
|
|
DOCUMENTS = "documents"
|
|
OTHER = "other"
|
|
|
|
|
|
class BackupStatus(str, Enum):
|
|
PENDING = "pending"
|
|
PROCESSING = "processing"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
class TripShareURL(BaseModel):
|
|
url: str
|
|
|
|
|
|
class LoginRegisterModel(BaseModel):
|
|
username: Annotated[
|
|
str,
|
|
StringConstraints(min_length=1, max_length=19, pattern=r"^[a-zA-Z0-9_-]+$"),
|
|
]
|
|
password: str
|
|
|
|
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
refresh_token: str
|
|
|
|
|
|
class PendingTOTP(BaseModel):
|
|
pending_code: str
|
|
username: str
|
|
|
|
|
|
class GooglePlaceResult(BaseModel):
|
|
name: str | None = None
|
|
lat: float | None = None
|
|
lng: float | None = None
|
|
price: float | None = None
|
|
allowsdog: bool | None = None
|
|
description: str | None = None
|
|
types: list[str] = []
|
|
|
|
|
|
class ImageBase(SQLModel):
|
|
filename: str
|
|
|
|
|
|
class Image(ImageBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE", index=True)
|
|
|
|
categories: list["Category"] = Relationship(back_populates="image")
|
|
places: list["Place"] = Relationship(back_populates="image")
|
|
trips: list["Trip"] = Relationship(back_populates="image")
|
|
tripitems: list["TripItem"] = Relationship(back_populates="image")
|
|
|
|
|
|
@event.listens_for(Image, "after_delete")
|
|
def mark_image_for_deletion(mapper, connection, target: Image):
|
|
session = object_session(target)
|
|
if not session:
|
|
return
|
|
if not hasattr(session, "_images_to_delete"):
|
|
session._images_to_delete = []
|
|
session._images_to_delete.append(target.filename)
|
|
|
|
|
|
class BackupBase(SQLModel):
|
|
completed_at: datetime | None = None
|
|
filename: str | None = None
|
|
error_message: str | None = None
|
|
file_size: int | None = None
|
|
|
|
|
|
class Backup(BackupBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE")
|
|
status: BackupStatus = Field(default=BackupStatus.PENDING)
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
|
|
|
|
@event.listens_for(Backup, "after_delete")
|
|
def mark_backup_for_deletion(mapper, connection, target: Backup):
|
|
session = object_session(target)
|
|
if not session:
|
|
return
|
|
if not hasattr(session, "_backups_to_delete"):
|
|
session._backups_to_delete = []
|
|
session._backups_to_delete.append(target.filename)
|
|
|
|
|
|
class BackupRead(BackupBase):
|
|
id: int
|
|
created_at: datetime
|
|
status: str
|
|
user: str
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: Backup) -> "BackupRead":
|
|
return cls(
|
|
id=obj.id,
|
|
completed_at=obj.completed_at,
|
|
created_at=obj.created_at,
|
|
error_message=obj.error_message,
|
|
filename=obj.filename,
|
|
file_size=obj.file_size,
|
|
status=obj.status,
|
|
user=obj.user,
|
|
)
|
|
|
|
|
|
class UserBase(SQLModel):
|
|
map_lat: float = settings.DEFAULT_MAP_LAT
|
|
map_lng: float = settings.DEFAULT_MAP_LNG
|
|
currency: str = settings.DEFAULT_CURRENCY
|
|
do_not_display: str = ""
|
|
tile_layer: str | None = None
|
|
mode_low_network: bool | None = True
|
|
mode_dark: bool | None = False
|
|
mode_gpx_in_place: bool | None = False
|
|
|
|
|
|
class User(UserBase, table=True):
|
|
username: str = Field(primary_key=True)
|
|
password: str
|
|
totp_enabled: bool = False
|
|
totp_secret: str | None = None
|
|
google_apikey: str | None = None
|
|
|
|
|
|
class UserUpdate(UserBase):
|
|
map_lat: float | None = None
|
|
map_lng: float | None = None
|
|
currency: str | None = None
|
|
do_not_display: list[str] | None = None
|
|
google_apikey: str | None = None
|
|
|
|
|
|
class UserRead(UserBase):
|
|
username: str
|
|
do_not_display: list[str]
|
|
totp_enabled: bool
|
|
google_apikey: bool
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: User) -> "UserRead":
|
|
return cls(
|
|
username=obj.username,
|
|
map_lat=obj.map_lat,
|
|
map_lng=obj.map_lng,
|
|
currency=obj.currency,
|
|
do_not_display=obj.do_not_display.split(",") if obj.do_not_display else [],
|
|
tile_layer=obj.tile_layer if obj.tile_layer else settings.DEFAULT_TILE,
|
|
mode_low_network=obj.mode_low_network,
|
|
mode_dark=obj.mode_dark,
|
|
mode_gpx_in_place=obj.mode_gpx_in_place,
|
|
totp_enabled=obj.totp_enabled,
|
|
google_apikey=True if obj.google_apikey else False,
|
|
)
|
|
|
|
|
|
class CategoryBase(SQLModel):
|
|
name: str
|
|
color: str | None = None
|
|
|
|
|
|
class Category(CategoryBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
image_id: int | None = Field(default=None, foreign_key="image.id", ondelete="CASCADE")
|
|
image: Image | None = Relationship(back_populates="categories")
|
|
places: list["Place"] = Relationship(back_populates="category")
|
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE", index=True)
|
|
|
|
|
|
class CategoryCreate(CategoryBase):
|
|
name: str
|
|
image: str | None = None
|
|
color: str | None = None
|
|
|
|
|
|
class CategoryUpdate(CategoryBase):
|
|
name: str | None = None
|
|
image: str | None = None
|
|
color: str | None = None
|
|
|
|
|
|
class CategoryRead(CategoryBase):
|
|
id: int
|
|
image: str | None
|
|
image_id: int | None
|
|
color: str
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: Category) -> "CategoryRead":
|
|
return cls(
|
|
id=obj.id,
|
|
name=obj.name,
|
|
image_id=obj.image_id,
|
|
image=_prefix_assets_url(obj.image.filename) if obj.image else "/favicon.png",
|
|
color=obj.color if obj.color else "#000000",
|
|
)
|
|
|
|
|
|
class TripPlaceLink(SQLModel, table=True):
|
|
trip_id: int = Field(foreign_key="trip.id", primary_key=True)
|
|
place_id: int = Field(foreign_key="place.id", primary_key=True, index=True)
|
|
|
|
|
|
class PlaceBase(SQLModel):
|
|
name: str
|
|
lat: float
|
|
lng: float
|
|
place: str
|
|
allowdog: bool | None = None
|
|
description: str | None = None
|
|
price: float | None = None
|
|
duration: int | None = None
|
|
favorite: bool | None = None
|
|
visited: bool | None = None
|
|
gpx: str | None = None
|
|
|
|
|
|
class Place(PlaceBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
cdate: date = Field(default_factory=lambda: datetime.now(UTC))
|
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE", index=True)
|
|
|
|
image_id: int | None = Field(default=None, foreign_key="image.id", ondelete="CASCADE")
|
|
image: Image | None = Relationship(back_populates="places")
|
|
|
|
category_id: int = Field(foreign_key="category.id", index=True)
|
|
category: Category | None = Relationship(back_populates="places")
|
|
|
|
trip_items: list["TripItem"] = Relationship(back_populates="place")
|
|
|
|
trips: list["Trip"] = Relationship(back_populates="places", link_model=TripPlaceLink)
|
|
|
|
__table_args__ = (Index("idx_place_user_category", "user", "category_id"),)
|
|
|
|
|
|
class PlaceCreate(PlaceBase):
|
|
image: str | None = None
|
|
category_id: int
|
|
|
|
|
|
class PlacesCreate(PlaceBase):
|
|
image: str | None = None
|
|
category: str
|
|
|
|
|
|
class PlaceUpdate(PlaceBase):
|
|
name: str | None = None
|
|
lat: float | None = None
|
|
lng: float | None = None
|
|
place: str | None = None
|
|
category_id: int | None = None
|
|
image: str | None = None
|
|
|
|
|
|
class PlaceRead(PlaceBase):
|
|
id: int
|
|
category: CategoryRead
|
|
image: str | None
|
|
image_id: int | None
|
|
user: str
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: Place, exclude_gpx=True) -> "PlaceRead":
|
|
return cls(
|
|
id=obj.id,
|
|
user=obj.user,
|
|
name=obj.name,
|
|
lat=obj.lat,
|
|
lng=obj.lng,
|
|
place=obj.place,
|
|
category=CategoryRead.serialize(obj.category),
|
|
allowdog=obj.allowdog,
|
|
description=obj.description,
|
|
price=obj.price,
|
|
duration=obj.duration,
|
|
visited=obj.visited,
|
|
image=_prefix_assets_url(obj.image.filename) if obj.image else None,
|
|
image_id=obj.image_id,
|
|
favorite=obj.favorite,
|
|
gpx=("1" if obj.gpx else None)
|
|
if exclude_gpx
|
|
else obj.gpx, # Generic PlaceRead. Avoid large resp.
|
|
)
|
|
|
|
|
|
class TripBase(SQLModel):
|
|
name: str
|
|
archived: bool | None = None
|
|
currency: str | None = settings.DEFAULT_CURRENCY
|
|
notes: str | None = None
|
|
archival_review: str | None = None
|
|
|
|
|
|
class Trip(TripBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE", index=True)
|
|
image_id: int | None = Field(default=None, foreign_key="image.id", ondelete="CASCADE")
|
|
|
|
image: Image | None = Relationship(back_populates="trips")
|
|
places: list["Place"] = Relationship(
|
|
back_populates="trips", sa_relationship_kwargs={"order_by": "Place.name"}, link_model=TripPlaceLink
|
|
)
|
|
days: list["TripDay"] = Relationship(
|
|
back_populates="trip",
|
|
sa_relationship_kwargs={"order_by": lambda: [TripDay.dt.asc().nulls_last(), TripDay.label]},
|
|
cascade_delete=True,
|
|
)
|
|
shares: list["TripShare"] = Relationship(back_populates="trip", cascade_delete=True)
|
|
packing_items: list["TripPackingListItem"] = Relationship(back_populates="trip", cascade_delete=True)
|
|
checklist_items: list["TripChecklistItem"] = Relationship(back_populates="trip", cascade_delete=True)
|
|
memberships: list["TripMember"] = Relationship(back_populates="trip", cascade_delete=True)
|
|
attachments: list["TripAttachment"] = Relationship(back_populates="trip", cascade_delete=True)
|
|
|
|
|
|
class TripCreate(TripBase):
|
|
image: str | None = None
|
|
place_ids: list[int] = []
|
|
|
|
|
|
class TripUpdate(TripBase):
|
|
name: str | None = None
|
|
image: str | None = None
|
|
place_ids: list[int] = []
|
|
|
|
|
|
class TripReadBase(TripBase):
|
|
id: int
|
|
image: str | None
|
|
image_id: int | None
|
|
days: int
|
|
collaborators: list["TripMemberRead"]
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: Trip) -> "TripRead":
|
|
return cls(
|
|
id=obj.id,
|
|
name=obj.name,
|
|
archived=obj.archived,
|
|
image=_prefix_assets_url(obj.image.filename) if obj.image else None,
|
|
image_id=obj.image_id,
|
|
days=len(obj.days),
|
|
collaborators=[TripMemberRead.serialize(m) for m in obj.memberships],
|
|
currency=obj.currency if obj.currency else settings.DEFAULT_CURRENCY,
|
|
)
|
|
|
|
|
|
class TripRead(TripBase):
|
|
id: int
|
|
image: str | None
|
|
image_id: int | None
|
|
days: list["TripDayRead"]
|
|
places: list["PlaceRead"]
|
|
collaborators: list["TripMemberRead"]
|
|
shared: bool
|
|
attachments: list["TripAttachmentRead"]
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: Trip) -> "TripRead":
|
|
return cls(
|
|
id=obj.id,
|
|
name=obj.name,
|
|
archived=obj.archived,
|
|
image=_prefix_assets_url(obj.image.filename) if obj.image else None,
|
|
image_id=obj.image_id,
|
|
days=[TripDayRead.serialize(day) for day in obj.days],
|
|
places=[PlaceRead.serialize(place) for place in obj.places],
|
|
collaborators=[TripMemberRead.serialize(m) for m in obj.memberships],
|
|
shared=bool(obj.shares),
|
|
currency=obj.currency if obj.currency else settings.DEFAULT_CURRENCY,
|
|
notes=obj.notes,
|
|
archival_review=obj.archival_review,
|
|
attachments=[TripAttachmentRead.serialize(att) for att in obj.attachments],
|
|
)
|
|
|
|
|
|
class TripMember(SQLModel, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
user: str = Field(foreign_key="user.username", ondelete="CASCADE")
|
|
invited_by: str | None = Field(default=None, foreign_key="user.username", ondelete="SET NULL")
|
|
invited_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
joined_at: datetime | None = None
|
|
|
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE", index=True)
|
|
trip: Trip | None = Relationship(back_populates="memberships")
|
|
|
|
__table_args__ = (Index("idx_tripmember_trip_user_joined", "trip_id", "user", "joined_at"),)
|
|
|
|
|
|
class TripMemberCreate(BaseModel):
|
|
user: str
|
|
|
|
|
|
class TripMemberRead(BaseModel):
|
|
user: str
|
|
invited_by: str | None = None
|
|
invited_at: datetime | None = None
|
|
joined_at: datetime | None = None
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: TripMember) -> "TripMemberRead":
|
|
return cls(
|
|
user=obj.user, invited_by=obj.invited_by, invited_at=obj.invited_at, joined_at=obj.joined_at
|
|
)
|
|
|
|
|
|
class TripInvitationRead(TripReadBase):
|
|
invited_by: str | None = None
|
|
invited_at: datetime
|
|
|
|
|
|
class TripDayBase(SQLModel):
|
|
label: str
|
|
dt: date | None = None
|
|
|
|
|
|
class TripDay(TripDayBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
|
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE", index=True)
|
|
trip: Trip | None = Relationship(back_populates="days")
|
|
|
|
items: list["TripItem"] = Relationship(back_populates="day", cascade_delete=True)
|
|
|
|
|
|
class TripDayRead(TripDayBase):
|
|
id: int
|
|
items: list["TripItemRead"]
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: TripDay) -> "TripDayRead":
|
|
return cls(
|
|
id=obj.id,
|
|
dt=obj.dt,
|
|
label=obj.label,
|
|
items=[TripItemRead.serialize(item) for item in obj.items],
|
|
)
|
|
|
|
|
|
class TripItemAttachmentLink(SQLModel, table=True):
|
|
item_id: int = Field(foreign_key="tripitem.id", ondelete="CASCADE", primary_key=True, index=True)
|
|
attachment_id: int = Field(foreign_key="tripattachment.id", ondelete="CASCADE", primary_key=True)
|
|
|
|
|
|
class TripItemBase(SQLModel):
|
|
time: Annotated[
|
|
str,
|
|
StringConstraints(min_length=2, max_length=5, pattern=r"^([01]\d|2[0-3])(:[0-5]\d)?$"),
|
|
]
|
|
text: str
|
|
comment: str | None = None
|
|
lat: float | None = None
|
|
price: float | None = None
|
|
lng: float | None = None
|
|
status: TripItemStatusEnum | None = None
|
|
gpx: str | None = None
|
|
|
|
@field_validator("time", mode="before")
|
|
def pad_mm_if_needed(cls, value: str) -> str:
|
|
if re.fullmatch(r"^([01]\d|2[0-3])$", value): # If it's just HH
|
|
return f"{value}:00"
|
|
return value
|
|
|
|
|
|
class TripItem(TripItemBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
|
|
place_id: int | None = Field(default=None, foreign_key="place.id")
|
|
place: Place | None = Relationship(back_populates="trip_items")
|
|
|
|
image_id: int | None = Field(default=None, foreign_key="image.id", ondelete="CASCADE")
|
|
image: Image | None = Relationship(back_populates="tripitems")
|
|
|
|
day_id: int = Field(foreign_key="tripday.id", ondelete="CASCADE", index=True)
|
|
day: TripDay | None = Relationship(back_populates="items")
|
|
|
|
paid_by: int | None = Field(default=None, foreign_key="user.username", ondelete="SET NULL")
|
|
|
|
attachments: list["TripAttachment"] = Relationship(
|
|
back_populates="items", link_model=TripItemAttachmentLink
|
|
)
|
|
|
|
|
|
class TripItemCreate(TripItemBase):
|
|
place: int | None = None
|
|
status: TripItemStatusEnum | None = None
|
|
image: str | None = None
|
|
paid_by: str | None = None
|
|
attachment_ids: list[int] = []
|
|
|
|
|
|
class TripItemUpdate(TripItemBase):
|
|
time: str | None = None
|
|
text: str | None = None
|
|
place: int | None = None
|
|
day_id: int | None = None
|
|
status: TripItemStatusEnum | None = None
|
|
image: str | None = None
|
|
paid_by: str | None = None
|
|
attachment_ids: list[int] = []
|
|
|
|
|
|
class TripItemRead(TripItemBase):
|
|
id: int
|
|
place: PlaceRead | None
|
|
day_id: int
|
|
status: TripItemStatusEnum | None
|
|
image: str | None
|
|
image_id: int | None
|
|
paid_by: str | None
|
|
attachments: list["TripAttachmentRead"]
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: TripItem) -> "TripItemRead":
|
|
return cls(
|
|
id=obj.id,
|
|
time=obj.time,
|
|
text=obj.text,
|
|
comment=obj.comment,
|
|
lat=obj.lat,
|
|
lng=obj.lng,
|
|
price=obj.price,
|
|
day_id=obj.day_id,
|
|
status=obj.status,
|
|
place=PlaceRead.serialize(obj.place) if obj.place else None,
|
|
image=_prefix_assets_url(obj.image.filename) if obj.image else None,
|
|
image_id=obj.image_id,
|
|
gpx=obj.gpx,
|
|
paid_by=obj.paid_by,
|
|
attachments=[TripAttachmentRead.serialize(att) for att in obj.attachments],
|
|
)
|
|
|
|
|
|
class TripShare(SQLModel, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
token: str = Field(index=True, unique=True)
|
|
|
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE")
|
|
trip: Trip | None = Relationship(back_populates="shares")
|
|
|
|
|
|
class TripPackingListItemBase(SQLModel):
|
|
text: str | None = None
|
|
qt: int | None = None
|
|
category: PackingListCategoryEnum | None = None
|
|
packed: bool | None = None
|
|
|
|
|
|
class TripPackingListItem(TripPackingListItemBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
|
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE", index=True)
|
|
trip: Trip | None = Relationship(back_populates="packing_items")
|
|
|
|
|
|
class TripPackingListItemCreate(TripPackingListItemBase):
|
|
packed: bool = False
|
|
|
|
|
|
class TripPackingListItemUpdate(TripPackingListItemBase): ...
|
|
|
|
|
|
class TripPackingListItemRead(TripPackingListItemBase):
|
|
id: int
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: "TripPackingListItem") -> "TripPackingListItemRead":
|
|
return cls(
|
|
id=obj.id,
|
|
text=obj.text,
|
|
qt=obj.qt,
|
|
category=obj.category,
|
|
packed=obj.packed,
|
|
)
|
|
|
|
|
|
class TripChecklistItemBase(SQLModel):
|
|
text: str | None = None
|
|
checked: bool | None = None
|
|
|
|
|
|
class TripChecklistItem(TripChecklistItemBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
|
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE", index=True)
|
|
trip: Trip | None = Relationship(back_populates="checklist_items")
|
|
|
|
|
|
class TripChecklistItemCreate(TripChecklistItemBase):
|
|
checked: bool = False
|
|
|
|
|
|
class TripChecklistItemUpdate(TripChecklistItemBase): ...
|
|
|
|
|
|
class TripChecklistItemRead(TripChecklistItemBase):
|
|
id: int
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: "TripChecklistItem") -> "TripChecklistItemRead":
|
|
return cls(
|
|
id=obj.id,
|
|
text=obj.text,
|
|
checked=obj.checked,
|
|
)
|
|
|
|
|
|
class TripAttachmentBase(SQLModel):
|
|
filename: str
|
|
file_size: int
|
|
stored_filename: str
|
|
|
|
|
|
class TripAttachment(TripAttachmentBase, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
uploaded_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
uploaded_by: str = Field(foreign_key="user.username", ondelete="CASCADE")
|
|
|
|
trip_id: int = Field(foreign_key="trip.id", ondelete="CASCADE", index=True)
|
|
trip: Trip | None = Relationship(back_populates="attachments")
|
|
|
|
items: list["TripItem"] = Relationship(back_populates="attachments", link_model=TripItemAttachmentLink)
|
|
|
|
|
|
@event.listens_for(TripAttachment, "after_delete")
|
|
def mark_attachment_for_deletion(mapper, connection, target: TripAttachment):
|
|
session = object_session(target)
|
|
if not session:
|
|
return
|
|
if not hasattr(session, "_attachments_to_delete"):
|
|
session._attachments_to_delete = []
|
|
session._attachments_to_delete.append(target)
|
|
|
|
|
|
class TripAttachmentCreate(TripAttachmentBase): ...
|
|
|
|
|
|
class TripAttachmentRead(TripAttachmentBase):
|
|
id: int
|
|
uploaded_by: str
|
|
|
|
@classmethod
|
|
def serialize(cls, obj: TripAttachment) -> "TripAttachmentRead":
|
|
return cls(
|
|
id=obj.id,
|
|
filename=obj.filename,
|
|
file_size=obj.file_size,
|
|
uploaded_by=obj.uploaded_by,
|
|
stored_filename=obj.stored_filename,
|
|
)
|