Komplett neu mit GPT

This commit is contained in:
Felix Zett 2025-08-13 21:10:04 +02:00
parent 269bfaab2c
commit e702418221
12 changed files with 498 additions and 165 deletions

View file

@ -7,5 +7,5 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

118
backend/crud.py Normal file
View file

@ -0,0 +1,118 @@
from __future__ import annotations
from typing import Iterable, List, Optional, Tuple
from uuid import UUID as UUID_t
from datetime import date
import re, ast, operator
from sqlalchemy.orm import Session, joinedload
from backend import models
ALLOWED_NAMES = {"days", "nights"}
ALLOWED_NODES = (
ast.Expression, ast.BinOp, ast.UnaryOp, ast.Num, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv,
ast.Mod, ast.Pow, ast.USub, ast.Load, ast.Name, ast.Constant, ast.Call
)
# Very small safe-eval for placeholders like {days * 10}
def _safe_eval(expr: str, ctx: dict) -> str:
node = ast.parse(expr, mode="eval")
for n in ast.walk(node):
if not isinstance(n, ALLOWED_NODES):
raise ValueError("disallowed expression")
if isinstance(n, ast.Name) and n.id not in ALLOWED_NAMES:
raise ValueError("unknown name")
if isinstance(n, ast.Call):
raise ValueError("calls not allowed")
val = eval(compile(node, "<expr>", "eval"), {"__builtins__": {}}, ctx)
return str(int(val)) if isinstance(val, (int, float)) and float(val).is_integer() else str(val)
_placeholder_re = re.compile(r"\{([^{}]+)\}")
def render_name(name_template: str, start: Optional[date], end: Optional[date]) -> str:
if not name_template:
return ""
days = nights = 0
if start and end:
days = (end - start).days + 1
nights = max(days - 1, 0)
ctx = {"days": days, "nights": nights}
def repl(m):
expr = m.group(1).strip()
try:
return _safe_eval(expr, ctx)
except Exception:
# if not evaluable, leave placeholder as-is
return m.group(0)
return _placeholder_re.sub(repl, name_template)
def items_for_trip(db: Session, user_id: UUID_t, selected_tag_ids: List[UUID_t]) -> List[models.Item]:
# Items ohne Tags (immer) + Items, die irgendeinen der selected_tags besitzen
q = (
db.query(models.Item)
.options(joinedload(models.Item.tags).joinedload(models.ItemTag.tag))
.filter(models.Item.user_id == user_id)
)
items = q.all()
selected_set = set(selected_tag_ids)
result: List[models.Item] = []
for it in items:
item_tag_ids = {link.tag_id for link in it.tags}
if not item_tag_ids:
result.append(it)
elif selected_set & item_tag_ids:
result.append(it)
return result
def generate_trip_items(
db: Session,
*,
trip: models.Trip,
selected_tag_ids: List[UUID_t],
marked_tag_ids: List[UUID_t],
) -> Tuple[List[models.TripItem], List[UUID_t]]:
"""Regeneriert TripItems für einen Trip. Löscht alte, legt neue an.
Gibt (created_ids, deleted_checked_ids) zurück."""
# Sammle bestehende checked Items, falls sie verschwinden
deleted_checked: List[UUID_t] = []
# Lösche alle existierenden TripItems und merke checked, die wegfallen
for ti in list(trip.trip_items):
if ti.checked:
deleted_checked.append(ti.id)
db.delete(ti)
db.flush()
items = items_for_trip(db, trip.user_id, selected_tag_ids)
created_ids: List[UUID_t] = []
marked_set = set(marked_tag_ids)
for it in items:
item_tag_ids = {link.tag_id for link in it.tags}
# bestimmen, ob dupliziert werden soll
intersection = item_tag_ids & marked_set
if marked_set and intersection:
per_tags = sorted(list(intersection))
else:
per_tags = [None] # gemeinsames Item (oder keine marked match)
for tag_id in per_tags:
calc = render_name(it.name, trip.start_date, trip.end_date)
ti = models.TripItem(
trip_id=trip.id,
item_id=it.id,
name_calculated=calc,
checked=False,
tag_id=tag_id,
)
db.add(ti)
db.flush()
created_ids.append(ti.id)
db.flush()
return created_ids, deleted_checked

View file

@ -1,14 +1,18 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import sessionmaker, declarative_base
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@db:5432/packlist" DATABASE_URL = "postgresql+psycopg2://postgres:postgres@db:5432/postgres"
engine = create_async_engine(DATABASE_URL) engine = create_engine(DATABASE_URL, future=True)
SessionLocal = sessionmaker( SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
bind=engine, class_=AsyncSession, expire_on_commit=False
)
Base = declarative_base() Base = declarative_base()
async def get_db(): # FastAPI dependency
async with SessionLocal() as db: from contextlib import contextmanager
def get_db():
db = SessionLocal()
try:
yield db yield db
finally:
db.close()

View file

@ -1,17 +1,22 @@
from fastapi import FastAPI from fastapi import FastAPI
from routes import items, trips from fastapi.middleware.cors import CORSMiddleware
from models import Base from backend.database import Base, engine
from database import engine from backend.routes import items, tags, trips, trip_items
app = FastAPI() # Create tables (for MVP without Alembic)
Base.metadata.create_all(bind=engine)
app = FastAPI(title="Packlist API (MVP)")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(tags.router)
app.include_router(items.router) app.include_router(items.router)
app.include_router(trips.router) app.include_router(trips.router)
app.include_router(trip_items.router)
@app.on_event("startup")
async def startup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@app.get("/")
def read_root():
return {"status": "running"}

View file

@ -1,51 +1,105 @@
from sqlalchemy import Column, String, Boolean, Date, ForeignKey, Table, Text
from sqlalchemy.dialects.postgresql import UUID, ARRAY
from sqlalchemy.orm import relationship, declarative_base
import uuid import uuid
from sqlalchemy import (
Base = declarative_base() Column, String, Boolean, Date, ForeignKey, UniqueConstraint
item_tags = Table(
"item_tags",
Base.metadata,
Column("item_id", UUID(as_uuid=True), ForeignKey("items.id")),
Column("tag_id", UUID(as_uuid=True), ForeignKey("tags.id")),
) )
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
items = relationship("Item", back_populates="user", cascade="all, delete")
tags = relationship("Tag", back_populates="user", cascade="all, delete")
trips = relationship("Trip", back_populates="user", cascade="all, delete")
class Item(Base): class Item(Base):
__tablename__ = "items" __tablename__ = "items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(Text, nullable=False) name = Column(String, nullable=False) # z.B. "Zahnbürste" oder "{days} x Vitamin D3"
tags = relationship("Tag", secondary=item_tags, back_populates="items")
user = relationship("User", back_populates="items")
tags = relationship("ItemTag", back_populates="item", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="item", cascade="all, delete")
class Tag(Base): class Tag(Base):
__tablename__ = "tags" __tablename__ = "tags"
__table_args__ = (UniqueConstraint("user_id", "name"),)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String, nullable=False, unique=False) name = Column(String, nullable=False)
items = relationship("Item", secondary=item_tags, back_populates="tags")
user = relationship("User", back_populates="tags")
items = relationship("ItemTag", back_populates="tag", cascade="all, delete")
trip_selected_tags = relationship("TripTagSelected", back_populates="tag", cascade="all, delete")
trip_marked_tags = relationship("TripTagMarked", back_populates="tag", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="tag")
class ItemTag(Base):
__tablename__ = "item_tags"
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True)
item = relationship("Item", back_populates="tags")
tag = relationship("Tag", back_populates="items")
class Trip(Base): class Trip(Base):
__tablename__ = "trips" __tablename__ = "trips"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False) user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
name = Column(String) name = Column(String)
start_date = Column(Date) start_date = Column(Date)
end_date = Column(Date) end_date = Column(Date)
selected_tags = Column(ARRAY(String))
marked_tags = Column(ARRAY(String)) user = relationship("User", back_populates="trips")
selected_tags = relationship("TripTagSelected", back_populates="trip", cascade="all, delete")
marked_tags = relationship("TripTagMarked", back_populates="trip", cascade="all, delete")
trip_items = relationship("TripItem", back_populates="trip", cascade="all, delete")
class TripTagSelected(Base):
__tablename__ = "trip_tag_selected"
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), primary_key=True)
trip = relationship("Trip", back_populates="selected_tags")
tag = relationship("Tag", back_populates="trip_selected_tags")
class TripTagMarked(Base):
__tablename__ = "trip_tag_marked"
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), primary_key=True)
trip = relationship("Trip", back_populates="marked_tags")
tag = relationship("Tag", back_populates="trip_marked_tags")
class TripItem(Base): class TripItem(Base):
__tablename__ = "trip_items" __tablename__ = "trip_items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id")) trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id", ondelete="CASCADE"), nullable=False)
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=True) item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=False)
tag = Column(String, nullable=True) # e.g. "#kristin" name_calculated = Column(String, nullable=False)
name = Column(Text) checked = Column(Boolean, nullable=False, default=False)
calculated_label = Column(Text) tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), nullable=True)
checked = Column(Boolean, default=False)
trip = relationship("Trip", back_populates="trip_items")
item = relationship("Item", back_populates="trip_items")
tag = relationship("Tag", back_populates="trip_items")

View file

View file

@ -1,87 +1,123 @@
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session, joinedload
from schemas import ItemCreate from uuid import UUID
from models import Item, Tag from backend.database import get_db
from database import get_db from backend import models
from sqlalchemy import select from backend.schemas import TripCreate, TripOut, TripUpdate, TripRegenerationResult
from sqlalchemy.orm import selectinload from backend.crud import generate_trip_items
from config import FIXED_USER_ID
router = APIRouter(prefix="/items", tags=["Items"]) router = APIRouter(prefix="/trips", tags=["trips"])
@router.post("/") @router.get("/", response_model=list[TripOut])
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_db)): def list_trips(db: Session = Depends(get_db)):
user_id = FIXED_USER_ID trips = (
db.query(models.Trip)
db_item = Item(id=uuid4(), name=item.name, user_id=user_id) .options(
joinedload(models.Trip.selected_tags).joinedload(models.TripTagSelected.tag),
tags = [] joinedload(models.Trip.marked_tags).joinedload(models.TripTagMarked.tag),
for tag_name in item.tag_names: )
result = await db.execute(select(Tag).where(Tag.name == tag_name, Tag.user_id == user_id)) .all()
tag = result.scalar_one_or_none()
if not tag:
tag = Tag(id=uuid4(), name=tag_name, user_id=user_id)
db.add(tag)
tags.append(tag)
db_item.tags = tags
db.add(db_item)
await db.commit()
return {"status": "item created", "item_id": str(db_item.id)}
@router.get("/")
async def read_items(db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(select(Item).where(Item.user_id == user_id).options(selectinload(Item.tags)))
items = result.scalars().all()
return {
"items": [
{
"id": str(item.id),
"name": item.name,
"tags": [tag.name for tag in item.tags]
}
for item in items
]
}
@router.put("/{item_id}")
async def update_item(item_id: str, item: ItemCreate, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(select(Item).where(Item.id == item_id, Item.user_id == user_id).options(selectinload(Item.tags)))
db_item = result.scalar_one_or_none()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
db_item.name = item.name
# Update tags
tags = []
for tag_name in item.tag_names:
result = await db.execute(select(Tag).where(Tag.name == tag_name, Tag.user_id == user_id))
tag = result.scalar_one_or_none()
if not tag:
tag = Tag(id=uuid4(), name=tag_name, user_id=user_id)
db.add(tag)
tags.append(tag)
db_item.tags = tags
await db.commit()
return {"status": "item updated", "item_id": str(db_item.id)}
@router.get("/{item_id}")
async def get_item(item_id: str, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID
result = await db.execute(
select(Item).where(Item.id == item_id, Item.user_id == user_id).options(selectinload(Item.tags))
) )
db_item = result.scalar_one_or_none() return [
if not db_item: TripOut(
raise HTTPException(status_code=404, detail="Item not found") id=t.id,
name=t.name,
start_date=t.start_date,
end_date=t.end_date,
selected_tags=[st.tag for st in t.selected_tags],
marked_tags=[mt.tag for mt in t.marked_tags],
)
for t in trips
]
@router.post("/", response_model=TripOut)
def create_trip(payload: TripCreate, db: Session = Depends(get_db)):
user = db.query(models.User).first()
if not user:
from uuid import uuid4
user = models.User(id=uuid4(), name="Demo")
db.add(user)
db.flush()
trip = models.Trip(user_id=user.id, name=payload.name, start_date=payload.start_date, end_date=payload.end_date)
db.add(trip)
db.flush()
# attach selected & marked
if payload.selected_tag_ids:
for tid in payload.selected_tag_ids:
db.add(models.TripTagSelected(trip_id=trip.id, tag_id=tid))
if payload.marked_tag_ids:
for tid in payload.marked_tag_ids:
db.add(models.TripTagMarked(trip_id=trip.id, tag_id=tid))
db.flush()
# generate items per rules
created_ids, _ = generate_trip_items(
db,
trip=trip,
selected_tag_ids=payload.selected_tag_ids,
marked_tag_ids=payload.marked_tag_ids,
)
db.commit()
# reload with relationships
trip = (
db.query(models.Trip)
.options(
joinedload(models.Trip.selected_tags).joinedload(models.TripTagSelected.tag),
joinedload(models.Trip.marked_tags).joinedload(models.TripTagMarked.tag),
)
.get(trip.id)
)
return TripOut(
id=trip.id,
name=trip.name,
start_date=trip.start_date,
end_date=trip.end_date,
selected_tags=[st.tag for st in trip.selected_tags],
marked_tags=[mt.tag for mt in trip.marked_tags],
)
@router.put("/{trip_id}/reconfigure", response_model=TripRegenerationResult)
def reconfigure_trip(trip_id: UUID, payload: TripUpdate, db: Session = Depends(get_db)):
trip = db.get(models.Trip, trip_id)
if not trip:
raise HTTPException(status_code=404, detail="Trip not found")
# update base fields
if payload.name is not None:
trip.name = payload.name
if payload.start_date is not None:
trip.start_date = payload.start_date
if payload.end_date is not None:
trip.end_date = payload.end_date
db.flush()
# update selected/marked join tables if provided
if payload.selected_tag_ids is not None:
# replace all
db.query(models.TripTagSelected).filter_by(trip_id=trip.id).delete()
for tid in payload.selected_tag_ids:
db.add(models.TripTagSelected(trip_id=trip.id, tag_id=tid))
if payload.marked_tag_ids is not None:
db.query(models.TripTagMarked).filter_by(trip_id=trip.id).delete()
for tid in payload.marked_tag_ids:
db.add(models.TripTagMarked(trip_id=trip.id, tag_id=tid))
db.flush()
# read back lists
sel_ids = [row.tag_id for row in db.query(models.TripTagSelected).filter_by(trip_id=trip.id).all()]
mrk_ids = [row.tag_id for row in db.query(models.TripTagMarked).filter_by(trip_id=trip.id).all()]
created_ids, deleted_checked = generate_trip_items(
db, trip=trip, selected_tag_ids=sel_ids, marked_tag_ids=mrk_ids
)
db.commit()
return { return {
"id": str(db_item.id), "trip_id": trip.id,
"name": db_item.name, "deleted_checked_trip_item_ids": deleted_checked,
"tags": [tag.name for tag in db_item.tags] "created_trip_item_ids": created_ids,
} }

33
backend/routes/tags.py Normal file
View file

@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from uuid import UUID
from backend.database import get_db
from backend import models
from backend.schemas import TagCreate, TagOut
router = APIRouter(prefix="/tags", tags=["tags"])
FIXED_USER_ID = None # will be created on seed; or set in your auth layer
@router.get("/", response_model=list[TagOut])
def list_tags(db: Session = Depends(get_db)):
tags = db.query(models.Tag).all()
return tags
@router.post("/", response_model=TagOut)
def create_tag(payload: TagCreate, db: Session = Depends(get_db)):
# For MVP: attach to first user (or create one)
user = db.query(models.User).first()
if not user:
from uuid import uuid4
user = models.User(id=uuid4(), name="Demo")
db.add(user)
db.flush()
existing = db.query(models.Tag).filter(models.Tag.user_id == user.id, models.Tag.name == payload.name).first()
if existing:
raise HTTPException(status_code=400, detail="Tag already exists")
tag = models.Tag(user_id=user.id, name=payload.name)
db.add(tag)
db.commit()
db.refresh(tag)
return tag

View file

@ -0,0 +1,51 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from uuid import UUID
from backend.database import get_db
from backend import models
from backend.schemas import TripItemOut
router = APIRouter(prefix="/trip-items", tags=["trip-items"])
@router.get("/by-trip/{trip_id}", response_model=list[TripItemOut])
def list_trip_items(trip_id: UUID, db: Session = Depends(get_db)):
trip = db.get(models.Trip, trip_id)
if not trip:
raise HTTPException(status_code=404, detail="Trip not found")
items = (
db.query(models.TripItem)
.options(joinedload(models.TripItem.tag))
.filter(models.TripItem.trip_id == trip_id)
.all()
)
return [
TripItemOut(
id=ti.id,
trip_id=ti.trip_id,
item_id=ti.item_id,
name_calculated=ti.name_calculated,
checked=ti.checked,
tag=ti.tag,
) for ti in items
]
@router.post("/{trip_item_id}/toggle", response_model=TripItemOut)
def toggle_trip_item(trip_item_id: UUID, db: Session = Depends(get_db)):
ti = (
db.query(models.TripItem)
.options(joinedload(models.TripItem.tag))
.get(trip_item_id)
)
if not ti:
raise HTTPException(status_code=404, detail="TripItem not found")
ti.checked = not ti.checked
db.commit()
db.refresh(ti)
return TripItemOut(
id=ti.id,
trip_id=ti.trip_id,
item_id=ti.item_id,
name_calculated=ti.name_calculated,
checked=ti.checked,
tag=ti.tag,
)

View file

@ -2,7 +2,7 @@ import re
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from schemas import TripCreate from schemas import TripCreate, TripItemOut, TripOut
from models import Item, Trip, TripItem from models import Item, Trip, TripItem
from database import get_db from database import get_db
from sqlalchemy import select from sqlalchemy import select
@ -11,7 +11,7 @@ from config import FIXED_USER_ID
router = APIRouter(prefix="/trips", tags=["Trips"]) router = APIRouter(prefix="/trips", tags=["Trips"])
@router.post("/") @router.post("/", response_model=TripOut)
async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)): async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID user_id = FIXED_USER_ID
@ -33,7 +33,7 @@ async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
nights = days - 1 nights = days - 1
# relevante Items # relevante Items
result = await db.execute(select(Item).where(Item.user_id == user_id)) result = await db.execute(select(Item).where(Item.user_id == user_id).options(selectinload(Item.tags)))
all_items = result.scalars().all() all_items = result.scalars().all()
trip_items = [] trip_items = []
@ -73,36 +73,22 @@ async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
db.add_all(trip_items) db.add_all(trip_items)
await db.commit() await db.commit()
return {"status": "trip created", "trip_id": str(db_trip.id)} # return {"status": "trip created", "trip_id": str(db_trip.id)}
return db_trip
@router.get("/{trip_id}/items") @router.get("/{trip_id}/items", response_model=list[TripItemOut])
async def get_trip_items(trip_id: UUID, db: AsyncSession = Depends(get_db)): async def get_trip_items(trip_id: UUID, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(TripItem).where(TripItem.trip_id == trip_id)) result = await db.execute(select(TripItem).where(TripItem.trip_id == trip_id).options(selectinload(TripItem.item)))
items = result.scalars().all() items = result.scalars().all()
return [ return [TripItemOut.model_validate(item) for item in items]
{
"label": item.calculated_label,
"tag": item.tag,
"checked": item.checked
} for item in items
]
@router.get("/") @router.get("/", response_model=list[TripOut])
async def get_trips(db: AsyncSession = Depends(get_db)): async def get_trips(db: AsyncSession = Depends(get_db)):
user_id = FIXED_USER_ID user_id = FIXED_USER_ID
result = await db.execute(select(Trip).where(Trip.user_id == user_id)) result = await db.execute(select(Trip).where(Trip.user_id == user_id))
trips = result.scalars().all() trips = result.scalars().all()
return [ return trips
{
"id": str(trip.id),
"name": trip.name,
"start_date": trip.start_date,
"end_date": trip.end_date,
"selected_tags": trip.selected_tags,
"marked_tags": trip.marked_tags
} for trip in trips
]
def replace_placeholders(text: str, days: int, nights: int) -> str: def replace_placeholders(text: str, days: int, nights: int) -> str:
def replacer(match): def replacer(match):

View file

@ -1,21 +1,65 @@
from typing import List, Optional
from uuid import UUID
from datetime import date from datetime import date
from typing import List
from pydantic import BaseModel from pydantic import BaseModel
class TagBase(BaseModel):
class TagCreate(BaseModel):
name: str name: str
class TagCreate(TagBase):
pass
class ItemCreate(BaseModel): class TagOut(TagBase):
id: UUID
class Config:
orm_mode = True
class ItemBase(BaseModel):
name: str name: str
tag_names: List[str]
class ItemCreate(ItemBase):
tag_ids: List[UUID] = []
class TripCreate(BaseModel): class ItemOut(ItemBase):
name: str id: UUID
start_date: date tags: List[TagOut] = []
end_date: date class Config:
selected_tags: List[str] orm_mode = True
marked_tags: List[str]
class TripBase(BaseModel):
name: Optional[str] = None
start_date: Optional[date] = None
end_date: Optional[date] = None
class TripCreate(TripBase):
selected_tag_ids: List[UUID] = []
marked_tag_ids: List[UUID] = []
class TripUpdate(BaseModel):
name: Optional[str] = None
start_date: Optional[date] = None
end_date: Optional[date] = None
selected_tag_ids: Optional[List[UUID]] = None
marked_tag_ids: Optional[List[UUID]] = None
class TripOut(TripBase):
id: UUID
selected_tags: List[TagOut]
marked_tags: List[TagOut]
class Config:
orm_mode = True
class TripItemOut(BaseModel):
id: UUID
trip_id: UUID
item_id: UUID
name_calculated: str
checked: bool
tag: Optional[TagOut] = None
class Config:
orm_mode = True
class TripRegenerationResult(BaseModel):
trip_id: UUID
deleted_checked_trip_item_ids: List[UUID] = []
created_trip_item_ids: List[UUID] = []

View file

@ -9,6 +9,8 @@ services:
- ./backend:/app - ./backend:/app
environment: environment:
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/packlist - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/packlist
depends_on:
- db
db: db:
image: postgres:15 image: postgres:15