✨ Trip sharing
This commit is contained in:
parent
719b837978
commit
356d9b8a59
@ -1 +1 @@
|
||||
__version__ = "1.11.0"
|
||||
__version__ = "1.12.0"
|
||||
|
||||
39
backend/trip/alembic/versions/77027ac49c26_trip_share.py
Normal file
39
backend/trip/alembic/versions/77027ac49c26_trip_share.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Trip share
|
||||
|
||||
Revision ID: 77027ac49c26
|
||||
Revises: d5fee6ec85c2
|
||||
Create Date: 2025-08-09 10:42:28.109690
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "77027ac49c26"
|
||||
down_revision = "d5fee6ec85c2"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"tripshare",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("trip_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["trip_id"], ["trip.id"], name=op.f("fk_tripshare_trip_id_trip"), ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_tripshare")),
|
||||
)
|
||||
with op.batch_alter_table("tripshare", schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f("ix_tripshare_token"), ["token"], unique=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("tripshare", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_tripshare_token"))
|
||||
|
||||
op.drop_table("tripshare")
|
||||
@ -39,6 +39,10 @@ class TripItemStatusEnum(str, Enum):
|
||||
OPTIONAL = "optional"
|
||||
|
||||
|
||||
class TripShareURL(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
class LoginRegisterModel(BaseModel):
|
||||
username: Annotated[
|
||||
str,
|
||||
@ -246,6 +250,7 @@ class Trip(TripBase, table=True):
|
||||
|
||||
places: list["Place"] = Relationship(back_populates="trips", link_model=TripPlaceLink)
|
||||
days: list["TripDay"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||
shares: list["TripShare"] = Relationship(back_populates="trip", cascade_delete=True)
|
||||
|
||||
|
||||
class TripCreate(TripBase):
|
||||
@ -283,6 +288,7 @@ class TripRead(TripBase):
|
||||
image_id: int | None
|
||||
days: list["TripDayRead"]
|
||||
places: list["PlaceRead"]
|
||||
shared: bool
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, obj: Trip) -> "TripRead":
|
||||
@ -294,6 +300,7 @@ class TripRead(TripBase):
|
||||
image_id=obj.image_id,
|
||||
days=[TripDayRead.serialize(day) for day in obj.days],
|
||||
places=[PlaceRead.serialize(place) for place in obj.places],
|
||||
shared=bool(obj.shares),
|
||||
)
|
||||
|
||||
|
||||
@ -386,3 +393,11 @@ class TripItemRead(TripItemBase):
|
||||
status=obj.status,
|
||||
place=PlaceRead.serialize(obj.place) if obj.place else None,
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@ -236,7 +236,9 @@ async def import_data(
|
||||
trip_place_id_map = {p["id"]: new_p.id for p, new_p in zip(data.get("places", []), places)}
|
||||
for trip in data.get("trips", []):
|
||||
trip_data = {
|
||||
key: trip[key] for key in trip.keys() if key not in {"id", "image", "image_id", "places", "days"}
|
||||
key: trip[key]
|
||||
for key in trip.keys()
|
||||
if key not in {"id", "image", "image_id", "places", "days", "shared"}
|
||||
}
|
||||
trip_data["user"] = current_user
|
||||
|
||||
|
||||
@ -8,9 +8,11 @@ from ..deps import SessionDep, get_current_username
|
||||
from ..models.models import (Image, Place, Trip, TripCreate, TripDay,
|
||||
TripDayBase, TripDayRead, TripItem,
|
||||
TripItemCreate, TripItemRead, TripItemUpdate,
|
||||
TripPlaceLink, TripRead, TripReadBase, TripUpdate)
|
||||
TripPlaceLink, TripRead, TripReadBase, TripShare,
|
||||
TripShareURL, TripUpdate)
|
||||
from ..security import verify_exists_and_owns
|
||||
from ..utils.utils import b64img_decode, remove_image, save_image_to_file
|
||||
from ..utils.utils import (b64img_decode, generate_urlsafe, remove_image,
|
||||
save_image_to_file)
|
||||
|
||||
router = APIRouter(prefix="/api/trips", tags=["trips"])
|
||||
|
||||
@ -338,3 +340,70 @@ def delete_tripitem(
|
||||
session.delete(db_item)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("/shared/{token}", response_model=TripRead)
|
||||
def read_shared_trip(
|
||||
session: SessionDep,
|
||||
token: str,
|
||||
) -> TripRead:
|
||||
share = session.exec(select(TripShare).where(TripShare.token == token)).first()
|
||||
if not share:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
db_trip = session.get(Trip, share.trip_id)
|
||||
return TripRead.serialize(db_trip)
|
||||
|
||||
|
||||
@router.get("/{trip_id}/share", response_model=TripShareURL)
|
||||
def get_shared_trip_url(
|
||||
session: SessionDep,
|
||||
trip_id: int,
|
||||
current_user: Annotated[str, Depends(get_current_username)],
|
||||
) -> TripShareURL:
|
||||
db_trip = session.get(Trip, trip_id)
|
||||
verify_exists_and_owns(current_user, db_trip)
|
||||
|
||||
share = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
||||
if not share:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
return {"url": f"/s/t/{share.token}"}
|
||||
|
||||
|
||||
@router.post("/{trip_id}/share", response_model=TripShareURL)
|
||||
def create_shared_trip(
|
||||
session: SessionDep,
|
||||
trip_id: int,
|
||||
current_user: Annotated[str, Depends(get_current_username)],
|
||||
) -> TripShareURL:
|
||||
db_trip = session.get(Trip, trip_id)
|
||||
verify_exists_and_owns(current_user, db_trip)
|
||||
|
||||
shared = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
||||
if shared:
|
||||
raise HTTPException(status_code=409, detail="The resource already exists")
|
||||
|
||||
token = generate_urlsafe()
|
||||
trip_share = TripShare(token=token, trip_id=trip_id, user=current_user)
|
||||
session.add(trip_share)
|
||||
session.commit()
|
||||
return {"url": f"/s/t/{token}"}
|
||||
|
||||
|
||||
@router.delete("/{trip_id}/share")
|
||||
def delete_shared_trip(
|
||||
session: SessionDep,
|
||||
trip_id: int,
|
||||
current_user: Annotated[str, Depends(get_current_username)],
|
||||
):
|
||||
db_trip = session.get(Trip, trip_id)
|
||||
verify_exists_and_owns(current_user, db_trip)
|
||||
|
||||
db_share = session.exec(select(TripShare).where(TripShare.trip_id == trip_id)).first()
|
||||
if not db_share:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
session.delete(db_share)
|
||||
session.commit()
|
||||
return {}
|
||||
|
||||
@ -2,6 +2,7 @@ import base64
|
||||
from datetime import date
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from secrets import token_urlsafe
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
@ -14,6 +15,10 @@ from ..config import Settings
|
||||
settings = Settings()
|
||||
|
||||
|
||||
def generate_urlsafe() -> str:
|
||||
return token_urlsafe(32)
|
||||
|
||||
|
||||
def generate_filename(format: str) -> str:
|
||||
return f"{uuid4()}.{format}"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user