itskovacs cc729722df 🐛 Fix typo
2025-08-19 19:59:44 +02:00

596 lines
18 KiB
Python

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, TripChecklistItem,
TripChecklistItemCreate, TripChecklistItemRead,
TripChecklistItemUpdate, TripCreate, TripDay,
TripDayBase, TripDayRead, TripItem,
TripItemCreate, TripItemRead, TripItemUpdate,
TripPackingListItem, TripPackingListItemCreate,
TripPackingListItemRead, TripPackingListItemUpdate,
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 {}
@router.get("/{trip_id}/checklist", response_model=list[TripChecklistItemRead])
def read_checklist(
session: SessionDep,
trip_id: int,
current_user: Annotated[str, Depends(get_current_username)],
) -> list[TripChecklistItemRead]:
items = session.exec(
select(TripChecklistItem).where(
TripChecklistItem.trip_id == trip_id, TripChecklistItem.user == current_user
)
)
return [TripChecklistItemRead.serialize(i) for i in items]
@router.get("/shared/{token}/checklist", response_model=list[TripChecklistItemRead])
def read_shared_trip_checklist(
session: SessionDep,
token: str,
) -> list[TripChecklistItemRead]:
items = session.exec(
select(TripChecklistItem).where(
TripChecklistItem.trip_id == _trip_from_token_or_404(session, token).trip_id
)
)
return [TripChecklistItemRead.serialize(i) for i in items]
@router.post("/{trip_id}/checklist", response_model=TripChecklistItemRead)
def create_checklist_item(
session: SessionDep,
trip_id: int,
data: TripChecklistItemCreate,
current_user: Annotated[str, Depends(get_current_username)],
) -> TripChecklistItemRead:
item = TripChecklistItem(
**data.model_dump(),
trip_id=trip_id,
user=current_user,
)
session.add(item)
session.commit()
session.refresh(item)
return TripChecklistItemRead.serialize(item)
@router.put("/{trip_id}/checklist/{id}", response_model=TripChecklistItemRead)
def update_checklist_item(
session: SessionDep,
item: TripChecklistItemUpdate,
trip_id: int,
id: int,
current_user: Annotated[str, Depends(get_current_username)],
) -> TripChecklistItemRead:
db_item = session.exec(
select(TripChecklistItem).where(
TripChecklistItem.id == id,
TripChecklistItem.trip_id == trip_id,
TripChecklistItem.user == current_user,
)
).one_or_none()
if not db_item:
raise HTTPException(status_code=404, detail="Not found")
item_data = 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 TripChecklistItemRead.serialize(db_item)
@router.delete("/{trip_id}/checklist/{id}")
def delete_checklist_item(
session: SessionDep,
trip_id: int,
id: int,
current_user: Annotated[str, Depends(get_current_username)],
):
item = session.exec(
select(TripChecklistItem).where(
TripChecklistItem.id == id,
TripChecklistItem.trip_id == trip_id,
TripChecklistItem.user == current_user,
)
).one_or_none()
if not item:
raise HTTPException(status_code=404, detail="Not found")
session.delete(item)
session.commit()
return {}