✨ Trip: item image and gpx
This commit is contained in:
parent
65b1e35186
commit
7869ba9931
37
backend/trip/alembic/versions/8775a65d510f_tripitem_image.py
Normal file
37
backend/trip/alembic/versions/8775a65d510f_tripitem_image.py
Normal 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 ###
|
||||||
33
backend/trip/alembic/versions/8e12410a0b8e_tripitem_gpx.py
Normal file
33
backend/trip/alembic/versions/8e12410a0b8e_tripitem_gpx.py
Normal 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 ###
|
||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user