Trip: item image and gpx

This commit is contained in:
itskovacs 2025-09-21 15:11:12 +02:00
parent 65b1e35186
commit 7869ba9931
4 changed files with 134 additions and 1 deletions

View File

@ -0,0 +1,37 @@
"""TripItem image
Revision ID: 8775a65d510f
Revises: 7e331b851cb7
Create Date: 2025-09-20 20:01:43.884115
"""
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op
# revision identifiers, used by Alembic.
revision = "8775a65d510f"
down_revision = "7e331b851cb7"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("tripitem", schema=None) as batch_op:
batch_op.add_column(sa.Column("image_id", sa.Integer(), nullable=True))
batch_op.create_foreign_key(
batch_op.f("fk_tripitem_image_id_image"), "image", ["image_id"], ["id"], ondelete="CASCADE"
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("tripitem", schema=None) as batch_op:
batch_op.drop_constraint(batch_op.f("fk_tripitem_image_id_image"), type_="foreignkey")
batch_op.drop_column("image_id")
# ### end Alembic commands ###

View File

@ -0,0 +1,33 @@
"""TripItem GPX
Revision ID: 8e12410a0b8e
Revises: 8775a65d510f
Create Date: 2025-09-20 19:21:18.294547
"""
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op
# revision identifiers, used by Alembic.
revision = "8e12410a0b8e"
down_revision = "8775a65d510f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("tripitem", schema=None) as batch_op:
batch_op.add_column(sa.Column("gpx", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("tripitem", schema=None) as batch_op:
batch_op.drop_column("gpx")
# ### end Alembic commands ###

View File

@ -75,6 +75,7 @@ class Image(ImageBase, table=True):
categories: list["Category"] = Relationship(back_populates="image") categories: list["Category"] = Relationship(back_populates="image")
places: list["Place"] = Relationship(back_populates="image") places: list["Place"] = Relationship(back_populates="image")
trips: list["Trip"] = Relationship(back_populates="image") trips: list["Trip"] = Relationship(back_populates="image")
tripitems: list["TripItem"] = Relationship(back_populates="image")
class UserBase(SQLModel): class UserBase(SQLModel):
@ -199,7 +200,6 @@ class Place(PlaceBase, table=True):
class PlaceCreate(PlaceBase): class PlaceCreate(PlaceBase):
image: str | None = None image: str | None = None
category_id: int category_id: int
gpx: str | None = None
class PlacesCreate(PlaceBase): class PlacesCreate(PlaceBase):
@ -393,6 +393,7 @@ class TripItemBase(SQLModel):
price: float | None = None price: float | None = None
lng: float | None = None lng: float | None = None
status: TripItemStatusEnum | None = None status: TripItemStatusEnum | None = None
gpx: str | None = None
@field_validator("time", mode="before") @field_validator("time", mode="before")
def pad_mm_if_needed(cls, value: str) -> str: def pad_mm_if_needed(cls, value: str) -> str:
@ -407,6 +408,9 @@ class TripItem(TripItemBase, table=True):
place_id: int | None = Field(default=None, foreign_key="place.id") place_id: int | None = Field(default=None, foreign_key="place.id")
place: Place | None = Relationship(back_populates="trip_items") 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") day_id: int = Field(foreign_key="tripday.id", ondelete="CASCADE")
day: TripDay | None = Relationship(back_populates="items") day: TripDay | None = Relationship(back_populates="items")
@ -414,6 +418,7 @@ class TripItem(TripItemBase, table=True):
class TripItemCreate(TripItemBase): class TripItemCreate(TripItemBase):
place: int | None = None place: int | None = None
status: TripItemStatusEnum | None = None status: TripItemStatusEnum | None = None
image: str | None = None
class TripItemUpdate(TripItemBase): class TripItemUpdate(TripItemBase):
@ -422,6 +427,7 @@ class TripItemUpdate(TripItemBase):
place: int | None = None place: int | None = None
day_id: int | None = None day_id: int | None = None
status: TripItemStatusEnum | None = None status: TripItemStatusEnum | None = None
image: str | None = None
class TripItemRead(TripItemBase): class TripItemRead(TripItemBase):
@ -429,6 +435,8 @@ class TripItemRead(TripItemBase):
place: PlaceRead | None place: PlaceRead | None
day_id: int day_id: int
status: TripItemStatusEnum | None status: TripItemStatusEnum | None
image: str | None
image_id: int | None
@classmethod @classmethod
def serialize(cls, obj: TripItem) -> "TripItemRead": def serialize(cls, obj: TripItem) -> "TripItemRead":
@ -443,6 +451,9 @@ class TripItemRead(TripItemBase):
day_id=obj.day_id, day_id=obj.day_id,
status=obj.status, status=obj.status,
place=PlaceRead.serialize(obj.place) if obj.place else None, 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,
) )

View File

@ -346,6 +346,18 @@ def create_tripitem(
status=item.status, status=item.status,
) )
if item.image:
image_bytes = b64img_decode(item.image)
filename = save_image_to_file(image_bytes, 0)
if not filename:
raise HTTPException(status_code=400, detail="Bad request")
image = Image(filename=filename, user=current_user)
session.add(image)
session.flush()
session.refresh(image)
new_item.image_id = image.id
if item.place is not None: if item.place is not None:
place_in_trip = any(place.id == item.place for place in db_trip.places) place_in_trip = any(place.id == item.place for place in db_trip.places)
if not place_in_trip: if not place_in_trip:
@ -382,6 +394,46 @@ def update_tripitem(
raise HTTPException(status_code=400, detail="Bad request") raise HTTPException(status_code=400, detail="Bad request")
item_data = item.model_dump(exclude_unset=True) item_data = item.model_dump(exclude_unset=True)
# TODO: Optimize logic; image=data: parse / image=none: remove / no image key: pass
if "image" in item_data: # no image key: pass
image_b64 = item_data.pop("image", None) # image=data: parse
if image_b64:
try:
image_bytes = b64img_decode(image_b64)
except Exception:
raise HTTPException(status_code=400, detail="Bad request")
filename = save_image_to_file(image_bytes, 0)
if not filename:
raise HTTPException(status_code=400, detail="Bad request")
image = Image(filename=filename, user=current_user)
session.add(image)
session.flush()
session.refresh(image)
if db_item.image_id:
old_image = session.get(Image, db_item.image_id)
try:
remove_image(old_image.filename)
session.delete(old_image)
db_item.image_id = None
session.refresh(db_item)
except Exception:
raise HTTPException(status_code=400, detail="Bad request")
db_item.image_id = image.id
else: # image=none: remove if previous
if getattr(db_item, "image_id", None):
old_image = session.get(Image, db_item.image_id)
try:
remove_image(old_image.filename)
session.delete(old_image)
db_item.image_id = None
session.refresh(db_item)
except Exception:
raise HTTPException(status_code=400, detail="Bad request")
place_id = item_data.pop("place", None) place_id = item_data.pop("place", None)
db_item.place_id = place_id db_item.place_id = place_id