from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from sqlmodel import select from ..config import settings from ..deps import SessionDep, get_current_username from ..models.models import (Image, Place, Trip, TripCreate, TripDay, TripDayBase, TripDayRead, TripItem, TripItemCreate, TripItemRead, TripItemUpdate, TripPackingListItem, TripPackingListItemCreate, TripPackingListItemRead, TripPackingListItemUpdate, TripPlaceLink, TripRead, TripReadBase, TripShare, TripShareURL, TripUpdate) from ..security import verify_exists_and_owns from ..utils.utils import (b64img_decode, generate_urlsafe, remove_image, save_image_to_file) router = APIRouter(prefix="/api/trips", tags=["trips"]) @router.get("", response_model=list[TripReadBase]) def read_trips( session: SessionDep, current_user: Annotated[str, Depends(get_current_username)] ) -> list[TripReadBase]: trips = session.exec(select(Trip).filter(Trip.user == current_user)) return [TripReadBase.serialize(trip) for trip in trips] @router.get("/{trip_id}", response_model=TripRead) def read_trip( session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)] ) -> TripRead: db_trip = session.get(Trip, trip_id) verify_exists_and_owns(current_user, db_trip) return TripRead.serialize(db_trip) @router.post("", response_model=TripReadBase) def create_trip( trip: TripCreate, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)] ) -> TripReadBase: new_trip = Trip( name=trip.name, user=current_user, ) if trip.image: image_bytes = b64img_decode(trip.image) filename = save_image_to_file(image_bytes, settings.TRIP_IMAGE_SIZE) if not filename: raise HTTPException(status_code=400, detail="Bad request") image = Image(filename=filename, user=current_user) session.add(image) session.commit() session.refresh(image) new_trip.image_id = image.id if trip.place_ids: for place_id in trip.place_ids: db_place = session.get(Place, place_id) verify_exists_and_owns(current_user, db_place) session.add(TripPlaceLink(trip_id=new_trip.id, place_id=db_place.id)) session.commit() session.add(new_trip) session.commit() session.refresh(new_trip) return TripReadBase.serialize(new_trip) @router.put("/{trip_id}", response_model=TripRead) def update_trip( session: SessionDep, trip_id: int, trip: TripUpdate, current_user: Annotated[str, Depends(get_current_username)], ) -> TripRead: db_trip = session.get(Trip, trip_id) verify_exists_and_owns(current_user, db_trip) if db_trip.archived and (trip.archived is not False): raise HTTPException(status_code=400, detail="Bad request") trip_data = trip.model_dump(exclude_unset=True) image_b64 = trip_data.pop("image", None) 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, settings.TRIP_IMAGE_SIZE) if not filename: raise HTTPException(status_code=400, detail="Bad request") image = Image(filename=filename, user=current_user) session.add(image) session.commit() session.refresh(image) if db_trip.image_id: old_image = session.get(Image, db_trip.image_id) try: remove_image(old_image.filename) session.delete(old_image) db_trip.image_id = None session.refresh(db_trip) except Exception: raise HTTPException(status_code=400, detail="Bad request") db_trip.image_id = image.id place_ids = trip_data.pop("place_ids", None) if place_ids is not None: # Could be empty [], so 'in' db_trip.places.clear() for place_id in place_ids: db_place = session.get(Place, place_id) verify_exists_and_owns(current_user, db_place) db_trip.places.append(db_place) item_place_ids = { item.place.id for day in db_trip.days for item in day.items if item.place is not None } invalid_place_ids = item_place_ids - set(place.id for place in db_trip.places) if invalid_place_ids: # TripItem references a Place that Trip.places misses raise HTTPException(status_code=400, detail="Bad Request") for key, value in trip_data.items(): setattr(db_trip, key, value) session.add(db_trip) session.commit() session.refresh(db_trip) return TripRead.serialize(db_trip) @router.delete("/{trip_id}") def delete_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) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") if db_trip.image: try: remove_image(db_trip.image.filename) session.delete(db_trip.image) except Exception: raise HTTPException( status_code=500, detail="Roses are red, violets are blue, if you're reading this, I'm sorry for you", ) session.delete(db_trip) session.commit() return {} @router.post("/{trip_id}/days", response_model=TripDayRead) def create_tripday( td: TripDayBase, trip_id: int, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)], ) -> TripDayRead: db_trip = session.get(Trip, trip_id) verify_exists_and_owns(current_user, db_trip) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") new_day = TripDay(label=td.label, trip_id=trip_id, user=current_user) session.add(new_day) session.commit() session.refresh(new_day) return TripDayRead.serialize(new_day) @router.put("/{trip_id}/days/{day_id}", response_model=TripDayRead) def update_tripday( td: TripDayBase, trip_id: int, day_id: int, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)], ) -> TripDayRead: db_trip = session.get(Trip, trip_id) verify_exists_and_owns(current_user, db_trip) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") db_day = session.get(TripDay, day_id) verify_exists_and_owns(current_user, db_day) if db_day.trip_id != trip_id: raise HTTPException(status_code=400, detail="Bad request") td_data = td.model_dump(exclude_unset=True) for key, value in td_data.items(): setattr(db_day, key, value) session.add(db_day) session.commit() session.refresh(db_day) return TripDayRead.serialize(db_day) @router.delete("/{trip_id}/days/{day_id}") def delete_tripday( session: SessionDep, trip_id: int, day_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) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") db_day = session.get(TripDay, day_id) verify_exists_and_owns(current_user, db_day) if db_day.trip_id != trip_id: raise HTTPException(status_code=400, detail="Bad request") session.delete(db_day) session.commit() return {} @router.post("/{trip_id}/days/{day_id}/items", response_model=TripItemRead) def create_tripitem( item: TripItemCreate, trip_id: int, day_id: int, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)], ) -> TripItemRead: db_trip = session.get(Trip, trip_id) verify_exists_and_owns(current_user, db_trip) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") db_day = session.get(TripDay, day_id) if db_day.trip_id != trip_id: raise HTTPException(status_code=400, detail="Bad request") new_item = TripItem( time=item.time, text=item.text, comment=item.comment, lat=item.lat, lng=item.lng, day_id=day_id, price=item.price, status=item.status, ) if item.place and item.place != "": place_in_trip = any(place.id == item.place for place in db_trip.places) if not place_in_trip: raise HTTPException(status_code=400, detail="Bad request") new_item.place_id = item.place session.add(new_item) session.commit() session.refresh(new_item) return TripItemRead.serialize(new_item) @router.put("/{trip_id}/days/{day_id}/items/{item_id}", response_model=TripItemRead) def update_tripitem( item: TripItemUpdate, trip_id: int, day_id: int, item_id: int, session: SessionDep, current_user: Annotated[str, Depends(get_current_username)], ) -> TripItemRead: db_trip = session.get(Trip, trip_id) verify_exists_and_owns(current_user, db_trip) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") db_day = session.get(TripDay, day_id) if db_day.trip_id != trip_id: raise HTTPException(status_code=400, detail="Bad request") db_item = session.get(TripItem, item_id) if db_item.day_id != day_id: raise HTTPException(status_code=400, detail="Bad request") item_data = item.model_dump(exclude_unset=True) place_id = item_data.pop("place", None) db_item.place_id = place_id if place_id is not None: place_in_trip = any(p.id == place_id for p in db_trip.places) if not place_in_trip: raise HTTPException(status_code=400, detail="Bad request") for key, value in item_data.items(): setattr(db_item, key, value) session.add(db_item) session.commit() session.refresh(db_item) return TripItemRead.serialize(db_item) @router.delete("/{trip_id}/days/{day_id}/items/{item_id}") def delete_tripitem( session: SessionDep, trip_id: int, day_id: int, item_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) if db_trip.archived: raise HTTPException(status_code=400, detail="Bad request") db_day = session.get(TripDay, day_id) if db_day.trip_id != trip_id: raise HTTPException(status_code=400, detail="Bad request") db_item = session.get(TripItem, item_id) if db_item.day_id != day_id: raise HTTPException(status_code=400, detail="Bad request") 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 {} @router.get("/{trip_id}/packing", response_model=list[TripPackingListItemRead]) def read_packing_list( session: SessionDep, trip_id: int, current_user: Annotated[str, Depends(get_current_username)], ) -> list[TripPackingListItemRead]: p_items = session.exec( select(TripPackingListItem) .where(TripPackingListItem.trip_id == trip_id, TripPackingListItem.user == current_user) .order_by(TripPackingListItem.id.asc()) ).all() return [TripPackingListItemRead.serialize(i) for i in p_items] @router.post("/{trip_id}/packing", response_model=TripPackingListItemRead) def create_packing_item( session: SessionDep, trip_id: int, data: TripPackingListItemCreate, current_user: Annotated[str, Depends(get_current_username)], ) -> TripPackingListItemRead: item = TripPackingListItem( **data.model_dump(), trip_id=trip_id, user=current_user, ) session.add(item) session.commit() session.refresh(item) return TripPackingListItemRead.serialize(item) @router.put("/{trip_id}/packing/{p_id}", response_model=TripPackingListItemRead) def update_packing_item( session: SessionDep, p_item: TripPackingListItemUpdate, trip_id: int, p_id: int, current_user: Annotated[str, Depends(get_current_username)], ) -> TripPackingListItemRead: db_item = session.exec( select(TripPackingListItem).where( TripPackingListItem.id == p_id, TripPackingListItem.trip_id == trip_id, TripPackingListItem.user == current_user, ) ).one_or_none() if not db_item: raise HTTPException(status_code=404, detail="Not found") item_data = p_item.model_dump(exclude_unset=True) for key, value in item_data.items(): setattr(db_item, key, value) session.add(db_item) session.commit() session.refresh(db_item) return TripPackingListItemRead.serialize(db_item) @router.delete("/{trip_id}/packing/{p_id}") def delete_packing_item( session: SessionDep, trip_id: int, p_id: int, current_user: Annotated[str, Depends(get_current_username)], ): item = session.exec( select(TripPackingListItem).where( TripPackingListItem.id == p_id, TripPackingListItem.trip_id == trip_id, TripPackingListItem.user == current_user, ) ).one_or_none() if not item: raise HTTPException(status_code=404, detail="Not found") session.delete(item) session.commit() return {}