Komplett neu mit GPT
This commit is contained in:
parent
269bfaab2c
commit
e702418221
12 changed files with 498 additions and 165 deletions
|
|
@ -7,5 +7,5 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||
|
||||
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
118
backend/crud.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@db:5432/packlist"
|
||||
DATABASE_URL = "postgresql+psycopg2://postgres:postgres@db:5432/postgres"
|
||||
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(
|
||||
bind=engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
engine = create_engine(DATABASE_URL, future=True)
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db():
|
||||
async with SessionLocal() as db:
|
||||
# FastAPI dependency
|
||||
from contextlib import contextmanager
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
|
@ -1,17 +1,22 @@
|
|||
from fastapi import FastAPI
|
||||
from routes import items, trips
|
||||
from models import Base
|
||||
from database import engine
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from backend.database import Base, 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(trips.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"}
|
||||
app.include_router(trip_items.router)
|
||||
|
|
@ -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
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
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 import (
|
||||
Column, String, Boolean, Date, ForeignKey, UniqueConstraint
|
||||
)
|
||||
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):
|
||||
__tablename__ = "items"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=False)
|
||||
name = Column(Text, nullable=False)
|
||||
tags = relationship("Tag", secondary=item_tags, back_populates="items")
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
name = Column(String, nullable=False) # z.B. "Zahnbürste" oder "{days} x Vitamin D3"
|
||||
|
||||
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):
|
||||
__tablename__ = "tags"
|
||||
__table_args__ = (UniqueConstraint("user_id", "name"),)
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(UUID(as_uuid=True), nullable=False)
|
||||
name = Column(String, nullable=False, unique=False)
|
||||
items = relationship("Item", secondary=item_tags, back_populates="tags")
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
|
||||
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):
|
||||
__tablename__ = "trips"
|
||||
|
||||
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)
|
||||
start_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):
|
||||
__tablename__ = "trip_items"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
trip_id = Column(UUID(as_uuid=True), ForeignKey("trips.id"))
|
||||
item_id = Column(UUID(as_uuid=True), ForeignKey("items.id"), nullable=True)
|
||||
tag = Column(String, nullable=True) # e.g. "#kristin"
|
||||
name = Column(Text)
|
||||
calculated_label = Column(Text)
|
||||
checked = Column(Boolean, default=False)
|
||||
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=False)
|
||||
name_calculated = Column(String, nullable=False)
|
||||
checked = Column(Boolean, nullable=False, default=False)
|
||||
tag_id = Column(UUID(as_uuid=True), ForeignKey("tags.id"), nullable=True)
|
||||
|
||||
trip = relationship("Trip", back_populates="trip_items")
|
||||
item = relationship("Item", back_populates="trip_items")
|
||||
tag = relationship("Tag", back_populates="trip_items")
|
||||
|
|
|
|||
0
backend/routes/__init__.py
Normal file
0
backend/routes/__init__.py
Normal file
|
|
@ -1,87 +1,123 @@
|
|||
from uuid import uuid4
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from schemas import ItemCreate
|
||||
from models import Item, Tag
|
||||
from database import get_db
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from config import FIXED_USER_ID
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from uuid import UUID
|
||||
from backend.database import get_db
|
||||
from backend import models
|
||||
from backend.schemas import TripCreate, TripOut, TripUpdate, TripRegenerationResult
|
||||
from backend.crud import generate_trip_items
|
||||
|
||||
router = APIRouter(prefix="/items", tags=["Items"])
|
||||
router = APIRouter(prefix="/trips", tags=["trips"])
|
||||
|
||||
@router.post("/")
|
||||
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_db)):
|
||||
user_id = FIXED_USER_ID
|
||||
|
||||
db_item = Item(id=uuid4(), name=item.name, user_id=user_id)
|
||||
|
||||
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
|
||||
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))
|
||||
@router.get("/", response_model=list[TripOut])
|
||||
def list_trips(db: Session = Depends(get_db)):
|
||||
trips = (
|
||||
db.query(models.Trip)
|
||||
.options(
|
||||
joinedload(models.Trip.selected_tags).joinedload(models.TripTagSelected.tag),
|
||||
joinedload(models.Trip.marked_tags).joinedload(models.TripTagMarked.tag),
|
||||
)
|
||||
db_item = result.scalar_one_or_none()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
TripOut(
|
||||
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 {
|
||||
"id": str(db_item.id),
|
||||
"name": db_item.name,
|
||||
"tags": [tag.name for tag in db_item.tags]
|
||||
"trip_id": trip.id,
|
||||
"deleted_checked_trip_item_ids": deleted_checked,
|
||||
"created_trip_item_ids": created_ids,
|
||||
}
|
||||
33
backend/routes/tags.py
Normal file
33
backend/routes/tags.py
Normal 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
|
||||
51
backend/routes/trip_items.py
Normal file
51
backend/routes/trip_items.py
Normal 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,
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@ import re
|
|||
from uuid import UUID, uuid4
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from schemas import TripCreate
|
||||
from schemas import TripCreate, TripItemOut, TripOut
|
||||
from models import Item, Trip, TripItem
|
||||
from database import get_db
|
||||
from sqlalchemy import select
|
||||
|
|
@ -11,7 +11,7 @@ from config import FIXED_USER_ID
|
|||
|
||||
router = APIRouter(prefix="/trips", tags=["Trips"])
|
||||
|
||||
@router.post("/")
|
||||
@router.post("/", response_model=TripOut)
|
||||
async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
|
||||
user_id = FIXED_USER_ID
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
|
|||
nights = days - 1
|
||||
|
||||
# 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()
|
||||
|
||||
trip_items = []
|
||||
|
|
@ -73,36 +73,22 @@ async def create_trip(trip: TripCreate, db: AsyncSession = Depends(get_db)):
|
|||
|
||||
db.add_all(trip_items)
|
||||
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)):
|
||||
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()
|
||||
return [
|
||||
{
|
||||
"label": item.calculated_label,
|
||||
"tag": item.tag,
|
||||
"checked": item.checked
|
||||
} for item in items
|
||||
]
|
||||
return [TripItemOut.model_validate(item) for item in items]
|
||||
|
||||
@router.get("/")
|
||||
@router.get("/", response_model=list[TripOut])
|
||||
async def get_trips(db: AsyncSession = Depends(get_db)):
|
||||
user_id = FIXED_USER_ID
|
||||
result = await db.execute(select(Trip).where(Trip.user_id == user_id))
|
||||
trips = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"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
|
||||
]
|
||||
return trips
|
||||
|
||||
def replace_placeholders(text: str, days: int, nights: int) -> str:
|
||||
def replacer(match):
|
||||
|
|
|
|||
|
|
@ -1,21 +1,65 @@
|
|||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from datetime import date
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TagCreate(BaseModel):
|
||||
class TagBase(BaseModel):
|
||||
name: str
|
||||
|
||||
class TagCreate(TagBase):
|
||||
pass
|
||||
|
||||
class ItemCreate(BaseModel):
|
||||
class TagOut(TagBase):
|
||||
id: UUID
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
class ItemBase(BaseModel):
|
||||
name: str
|
||||
tag_names: List[str]
|
||||
|
||||
class ItemCreate(ItemBase):
|
||||
tag_ids: List[UUID] = []
|
||||
|
||||
class TripCreate(BaseModel):
|
||||
name: str
|
||||
start_date: date
|
||||
end_date: date
|
||||
selected_tags: List[str]
|
||||
marked_tags: List[str]
|
||||
class ItemOut(ItemBase):
|
||||
id: UUID
|
||||
tags: List[TagOut] = []
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
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] = []
|
||||
|
|
@ -9,6 +9,8 @@ services:
|
|||
- ./backend:/app
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/packlist
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: postgres:15
|
||||
|
|
|
|||
Loading…
Reference in a new issue