Initial commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import select
|
||||
|
||||
from app.deps import SessionDep
|
||||
from app.models import User
|
||||
from app.schemas import Token, UserCreate, UserRead
|
||||
from app.security import create_access_token, get_password_hash, verify_password
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
|
||||
def register(user_in: UserCreate, session: SessionDep):
|
||||
existing_user = session.exec(select(User).where(User.username == user_in.username)).first()
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered")
|
||||
|
||||
user = User(username=user_in.username, hashed_password=get_password_hash(user_in.password))
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
def login(
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
session: SessionDep,
|
||||
):
|
||||
user = session.exec(select(User).where(User.username == form_data.username)).first()
|
||||
if not user or not verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": user.username})
|
||||
return Token(access_token=access_token)
|
||||
@@ -0,0 +1,218 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.access import product_barcode_accessible, user_can_access_dish
|
||||
from app.deps import CurrentUserDep, SessionDep
|
||||
from app.models import Dish, DishIngredient, DishShare, Product, User
|
||||
from app.schemas import DishCreate, DishIngredientRead, DishRead, DishShareCreate, DishShareRead
|
||||
|
||||
router = APIRouter(prefix="/api/dishes", tags=["dishes"])
|
||||
|
||||
NUTRIENT_FIELDS = ["calories", "carbs", "protein", "fat", "sugar", "fiber", "saturated_fat", "salt"]
|
||||
|
||||
|
||||
def _compute_nutrition_per_100g(session: Session, ingredients: list, user_id: int) -> tuple[dict, float]:
|
||||
total_weight = sum(ingredient.amount_g for ingredient in ingredients)
|
||||
if total_weight <= 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Zutaten ergeben insgesamt 0g")
|
||||
|
||||
totals = dict.fromkeys(NUTRIENT_FIELDS, 0.0)
|
||||
for ingredient in ingredients:
|
||||
product = session.exec(select(Product).where(Product.barcode == ingredient.barcode)).first()
|
||||
if product is None or not product_barcode_accessible(session, ingredient.barcode, user_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Zutat mit Barcode {ingredient.barcode} nicht gefunden",
|
||||
)
|
||||
factor = ingredient.amount_g / 100.0
|
||||
for field in NUTRIENT_FIELDS:
|
||||
totals[field] += getattr(product, field) * factor
|
||||
|
||||
per_100g = {field: totals[field] / total_weight * 100 for field in NUTRIENT_FIELDS}
|
||||
return per_100g, total_weight
|
||||
|
||||
|
||||
def _get_owned_dish(session: Session, dish_id: int, user_id: int) -> Dish:
|
||||
dish = session.get(Dish, dish_id)
|
||||
if dish is None or dish.user_id != user_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gericht nicht gefunden")
|
||||
return dish
|
||||
|
||||
|
||||
def _get_accessible_dish(session: Session, dish_id: int, user_id: int) -> Dish:
|
||||
dish = session.get(Dish, dish_id)
|
||||
if dish is None or not user_can_access_dish(session, dish, user_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gericht nicht gefunden")
|
||||
return dish
|
||||
|
||||
|
||||
def _to_dish_read(session: Session, dish: Dish, current_user_id: int) -> DishRead:
|
||||
product = session.exec(select(Product).where(Product.barcode == dish.product_barcode)).first()
|
||||
ingredient_rows = session.exec(select(DishIngredient).where(DishIngredient.dish_id == dish.id)).all()
|
||||
owner = session.get(User, dish.user_id)
|
||||
|
||||
ingredients = []
|
||||
for row in ingredient_rows:
|
||||
ingredient_product = session.exec(select(Product).where(Product.barcode == row.product_barcode)).first()
|
||||
ingredients.append(
|
||||
DishIngredientRead(
|
||||
barcode=row.product_barcode,
|
||||
name=ingredient_product.name if ingredient_product else "Unbekannt",
|
||||
amount_g=row.amount_g,
|
||||
**{
|
||||
field: (getattr(ingredient_product, field) if ingredient_product else 0.0)
|
||||
for field in NUTRIENT_FIELDS
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return DishRead(
|
||||
id=dish.id,
|
||||
name=dish.name,
|
||||
instructions=dish.instructions,
|
||||
barcode=dish.product_barcode,
|
||||
total_weight_g=dish.total_weight_g,
|
||||
ingredients=ingredients,
|
||||
is_owner=dish.user_id == current_user_id,
|
||||
owner_username=owner.username if owner else "?",
|
||||
**{field: getattr(product, field) for field in NUTRIENT_FIELDS},
|
||||
)
|
||||
|
||||
|
||||
def _replace_ingredients(session: Session, dish: Dish, ingredients_in: list) -> None:
|
||||
existing = session.exec(select(DishIngredient).where(DishIngredient.dish_id == dish.id)).all()
|
||||
for row in existing:
|
||||
session.delete(row)
|
||||
session.flush()
|
||||
for ingredient in ingredients_in:
|
||||
session.add(DishIngredient(dish_id=dish.id, product_barcode=ingredient.barcode, amount_g=ingredient.amount_g))
|
||||
|
||||
|
||||
@router.post("", response_model=DishRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_dish(dish_in: DishCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
per_100g, total_weight = _compute_nutrition_per_100g(session, dish_in.ingredients, current_user.id)
|
||||
|
||||
barcode = f"dish-{uuid4().hex[:12]}"
|
||||
product = Product(barcode=barcode, name=dish_in.name, **per_100g)
|
||||
session.add(product)
|
||||
|
||||
dish = Dish(
|
||||
user_id=current_user.id,
|
||||
product_barcode=barcode,
|
||||
name=dish_in.name,
|
||||
instructions=dish_in.instructions,
|
||||
total_weight_g=total_weight,
|
||||
)
|
||||
session.add(dish)
|
||||
session.flush()
|
||||
|
||||
_replace_ingredients(session, dish, dish_in.ingredients)
|
||||
|
||||
session.commit()
|
||||
session.refresh(dish)
|
||||
return _to_dish_read(session, dish, current_user.id)
|
||||
|
||||
|
||||
@router.get("", response_model=list[DishRead])
|
||||
def list_dishes(current_user: CurrentUserDep, session: SessionDep):
|
||||
owned = session.exec(select(Dish).where(Dish.user_id == current_user.id)).all()
|
||||
shared_dish_ids = session.exec(
|
||||
select(DishShare.dish_id).where(DishShare.shared_with_user_id == current_user.id)
|
||||
).all()
|
||||
shared = session.exec(select(Dish).where(Dish.id.in_(shared_dish_ids))).all() if shared_dish_ids else []
|
||||
dishes = sorted(owned + shared, key=lambda dish: dish.name.lower())
|
||||
return [_to_dish_read(session, dish, current_user.id) for dish in dishes]
|
||||
|
||||
|
||||
@router.get("/{dish_id}", response_model=DishRead)
|
||||
def get_dish(dish_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_accessible_dish(session, dish_id, current_user.id)
|
||||
return _to_dish_read(session, dish, current_user.id)
|
||||
|
||||
|
||||
@router.put("/{dish_id}", response_model=DishRead)
|
||||
def update_dish(dish_id: int, dish_in: DishCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
per_100g, total_weight = _compute_nutrition_per_100g(session, dish_in.ingredients, current_user.id)
|
||||
|
||||
product = session.exec(select(Product).where(Product.barcode == dish.product_barcode)).first()
|
||||
product.name = dish_in.name
|
||||
for field in NUTRIENT_FIELDS:
|
||||
setattr(product, field, per_100g[field])
|
||||
session.add(product)
|
||||
|
||||
dish.name = dish_in.name
|
||||
dish.instructions = dish_in.instructions
|
||||
dish.total_weight_g = total_weight
|
||||
session.add(dish)
|
||||
|
||||
_replace_ingredients(session, dish, dish_in.ingredients)
|
||||
|
||||
session.commit()
|
||||
session.refresh(dish)
|
||||
return _to_dish_read(session, dish, current_user.id)
|
||||
|
||||
|
||||
@router.delete("/{dish_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_dish(dish_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
|
||||
ingredient_rows = session.exec(select(DishIngredient).where(DishIngredient.dish_id == dish.id)).all()
|
||||
for row in ingredient_rows:
|
||||
session.delete(row)
|
||||
|
||||
share_rows = session.exec(select(DishShare).where(DishShare.dish_id == dish.id)).all()
|
||||
for row in share_rows:
|
||||
session.delete(row)
|
||||
|
||||
product = session.exec(select(Product).where(Product.barcode == dish.product_barcode)).first()
|
||||
session.delete(dish)
|
||||
if product:
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get("/{dish_id}/shares", response_model=list[DishShareRead])
|
||||
def list_shares(dish_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
shares = session.exec(select(DishShare).where(DishShare.dish_id == dish.id)).all()
|
||||
users = {user.id: user for user in session.exec(select(User)).all()}
|
||||
return [DishShareRead(username=users[s.shared_with_user_id].username) for s in shares if s.shared_with_user_id in users]
|
||||
|
||||
|
||||
@router.post("/{dish_id}/shares", response_model=list[DishShareRead], status_code=status.HTTP_201_CREATED)
|
||||
def add_share(dish_id: int, share_in: DishShareCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
|
||||
target = session.exec(select(User).where(User.username == share_in.username)).first()
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Nutzer nicht gefunden")
|
||||
if target.id == current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Kann nicht mit sich selbst geteilt werden")
|
||||
|
||||
existing = session.exec(
|
||||
select(DishShare).where(DishShare.dish_id == dish.id, DishShare.shared_with_user_id == target.id)
|
||||
).first()
|
||||
if existing is None:
|
||||
session.add(DishShare(dish_id=dish.id, shared_with_user_id=target.id))
|
||||
session.commit()
|
||||
|
||||
return list_shares(dish_id, current_user, session)
|
||||
|
||||
|
||||
@router.delete("/{dish_id}/shares/{username}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def remove_share(dish_id: int, username: str, current_user: CurrentUserDep, session: SessionDep):
|
||||
dish = _get_owned_dish(session, dish_id, current_user.id)
|
||||
|
||||
target = session.exec(select(User).where(User.username == username)).first()
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Nutzer nicht gefunden")
|
||||
|
||||
row = session.exec(
|
||||
select(DishShare).where(DishShare.dish_id == dish.id, DishShare.shared_with_user_id == target.id)
|
||||
).first()
|
||||
if row:
|
||||
session.delete(row)
|
||||
session.commit()
|
||||
@@ -0,0 +1,176 @@
|
||||
from datetime import date as date_type
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.access import product_barcode_accessible
|
||||
from app.deps import CurrentUserDep, SessionDep
|
||||
from app.models import FoodLog, Product
|
||||
from app.schemas import (
|
||||
DailyHistoryEntry,
|
||||
DailyLogResponse,
|
||||
DailyTotals,
|
||||
HistoryResponse,
|
||||
LogCreate,
|
||||
LogRead,
|
||||
LogUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
||||
|
||||
|
||||
def _day_bounds(day: date_type) -> tuple[datetime, datetime]:
|
||||
start = datetime(day.year, day.month, day.day, tzinfo=timezone.utc)
|
||||
return start, start + timedelta(days=1)
|
||||
|
||||
|
||||
def _totals_for_logs(logs: list[FoodLog]) -> DailyTotals:
|
||||
return DailyTotals(
|
||||
calories=sum(log.calories for log in logs),
|
||||
carbs=sum(log.carbs for log in logs),
|
||||
protein=sum(log.protein for log in logs),
|
||||
fat=sum(log.fat for log in logs),
|
||||
sugar=sum(log.sugar for log in logs),
|
||||
fiber=sum(log.fiber for log in logs),
|
||||
saturated_fat=sum(log.saturated_fat for log in logs),
|
||||
salt=sum(log.salt for log in logs),
|
||||
)
|
||||
|
||||
|
||||
def _day_response(session: Session, user_id: int, day: date_type) -> DailyLogResponse:
|
||||
start, end = _day_bounds(day)
|
||||
logs = session.exec(
|
||||
select(FoodLog)
|
||||
.where(FoodLog.user_id == user_id)
|
||||
.where(FoodLog.timestamp >= start)
|
||||
.where(FoodLog.timestamp < end)
|
||||
.order_by(FoodLog.timestamp)
|
||||
).all()
|
||||
return DailyLogResponse(logs=logs, totals=_totals_for_logs(logs))
|
||||
|
||||
|
||||
@router.post("", response_model=LogRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_log(log_in: LogCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
product = None
|
||||
if log_in.barcode:
|
||||
product = session.exec(select(Product).where(Product.barcode == log_in.barcode)).first()
|
||||
elif log_in.name:
|
||||
product = session.exec(select(Product).where(Product.name == log_in.name)).first()
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="barcode oder name erforderlich")
|
||||
|
||||
if product is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Produkt nicht gefunden. Zuerst per GET /api/products/{barcode} suchen oder per POST /api/products anlegen.",
|
||||
)
|
||||
|
||||
if not product_barcode_accessible(session, product.barcode, current_user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Produkt nicht gefunden. Zuerst per GET /api/products/{barcode} suchen oder per POST /api/products anlegen.",
|
||||
)
|
||||
|
||||
factor = log_in.amount_g / 100.0
|
||||
log = FoodLog(
|
||||
user_id=current_user.id,
|
||||
barcode=product.barcode,
|
||||
name=product.name,
|
||||
meal_type=log_in.meal_type,
|
||||
amount_g=log_in.amount_g,
|
||||
calories=product.calories * factor,
|
||||
carbs=product.carbs * factor,
|
||||
protein=product.protein * factor,
|
||||
fat=product.fat * factor,
|
||||
sugar=product.sugar * factor,
|
||||
fiber=product.fiber * factor,
|
||||
saturated_fat=product.saturated_fat * factor,
|
||||
salt=product.salt * factor,
|
||||
)
|
||||
session.add(log)
|
||||
session.commit()
|
||||
session.refresh(log)
|
||||
return log
|
||||
|
||||
|
||||
def _get_owned_log(session: Session, log_id: int, user_id: int) -> FoodLog:
|
||||
log = session.get(FoodLog, log_id)
|
||||
if log is None or log.user_id != user_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Eintrag nicht gefunden")
|
||||
return log
|
||||
|
||||
|
||||
@router.put("/{log_id}", response_model=LogRead)
|
||||
def update_log(log_id: int, log_in: LogUpdate, current_user: CurrentUserDep, session: SessionDep):
|
||||
log = _get_owned_log(session, log_id, current_user.id)
|
||||
|
||||
if log_in.amount_g is not None and log_in.amount_g != log.amount_g:
|
||||
factor = log_in.amount_g / log.amount_g
|
||||
log.calories *= factor
|
||||
log.carbs *= factor
|
||||
log.protein *= factor
|
||||
log.fat *= factor
|
||||
log.sugar *= factor
|
||||
log.fiber *= factor
|
||||
log.saturated_fat *= factor
|
||||
log.salt *= factor
|
||||
log.amount_g = log_in.amount_g
|
||||
|
||||
if log_in.meal_type is not None:
|
||||
log.meal_type = log_in.meal_type
|
||||
|
||||
session.add(log)
|
||||
session.commit()
|
||||
session.refresh(log)
|
||||
return log
|
||||
|
||||
|
||||
@router.delete("/{log_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_log(log_id: int, current_user: CurrentUserDep, session: SessionDep):
|
||||
log = _get_owned_log(session, log_id, current_user.id)
|
||||
session.delete(log)
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get("/today", response_model=DailyLogResponse)
|
||||
def get_today_logs(current_user: CurrentUserDep, session: SessionDep):
|
||||
today = datetime.now(timezone.utc).date()
|
||||
return _day_response(session, current_user.id, today)
|
||||
|
||||
|
||||
@router.get("/history", response_model=HistoryResponse)
|
||||
def get_history(
|
||||
current_user: CurrentUserDep,
|
||||
session: SessionDep,
|
||||
days: int = Query(default=30, ge=1, le=365),
|
||||
):
|
||||
today = datetime.now(timezone.utc).date()
|
||||
range_start_day = today - timedelta(days=days - 1)
|
||||
range_start, _ = _day_bounds(range_start_day)
|
||||
_, range_end = _day_bounds(today)
|
||||
|
||||
logs = session.exec(
|
||||
select(FoodLog)
|
||||
.where(FoodLog.user_id == current_user.id)
|
||||
.where(FoodLog.timestamp >= range_start)
|
||||
.where(FoodLog.timestamp < range_end)
|
||||
).all()
|
||||
|
||||
logs_by_day: dict[date_type, list[FoodLog]] = {}
|
||||
for log in logs:
|
||||
logs_by_day.setdefault(log.timestamp.date(), []).append(log)
|
||||
|
||||
result_days = [
|
||||
DailyHistoryEntry(
|
||||
date=range_start_day + timedelta(days=offset),
|
||||
totals=_totals_for_logs(logs_by_day.get(range_start_day + timedelta(days=offset), [])),
|
||||
)
|
||||
for offset in range(days)
|
||||
]
|
||||
return HistoryResponse(days=result_days)
|
||||
|
||||
|
||||
@router.get("/day/{day}", response_model=DailyLogResponse)
|
||||
def get_day_logs(day: date_type, current_user: CurrentUserDep, session: SessionDep):
|
||||
return _day_response(session, current_user.id, day)
|
||||
@@ -0,0 +1,137 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from sqlmodel import select
|
||||
|
||||
from app.access import product_barcode_accessible
|
||||
from app.deps import CurrentUserDep, SessionDep
|
||||
from app.models import FoodLog, Product
|
||||
from app.schemas import ProductCreate, ProductRead, ProductSearchResult
|
||||
|
||||
router = APIRouter(prefix="/api/products", tags=["products"])
|
||||
|
||||
OFF_API_URL = "https://world.openfoodfacts.org/api/v2/product/{barcode}.json"
|
||||
|
||||
|
||||
def _extract_macros(nutriments: dict) -> dict:
|
||||
calories = nutriments.get("energy-kcal_100g")
|
||||
if calories is None:
|
||||
energy_kj = nutriments.get("energy_100g")
|
||||
calories = energy_kj / 4.184 if energy_kj is not None else 0.0
|
||||
|
||||
return {
|
||||
"calories": calories,
|
||||
"carbs": nutriments.get("carbohydrates_100g", 0.0) or 0.0,
|
||||
"protein": nutriments.get("proteins_100g", 0.0) or 0.0,
|
||||
"fat": nutriments.get("fat_100g", 0.0) or 0.0,
|
||||
"sugar": nutriments.get("sugars_100g", 0.0) or 0.0,
|
||||
"fiber": nutriments.get("fiber_100g", 0.0) or 0.0,
|
||||
"saturated_fat": nutriments.get("saturated-fat_100g", 0.0) or 0.0,
|
||||
"salt": nutriments.get("salt_100g", 0.0) or 0.0,
|
||||
}
|
||||
|
||||
|
||||
async def _fetch_from_open_food_facts(barcode: str) -> Product | None:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(OFF_API_URL.format(barcode=barcode))
|
||||
except httpx.RequestError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Open Food Facts ist gerade nicht erreichbar",
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
data = response.json()
|
||||
if data.get("status") != 1:
|
||||
return None
|
||||
|
||||
product_data = data.get("product", {})
|
||||
macros = _extract_macros(product_data.get("nutriments", {}))
|
||||
name = product_data.get("product_name") or product_data.get("generic_name") or "Unbekanntes Produkt"
|
||||
|
||||
return Product(barcode=barcode, name=name, **macros)
|
||||
|
||||
|
||||
@router.post("", response_model=ProductRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_product(product_in: ProductCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
barcode = product_in.barcode or f"manual-{uuid4().hex[:12]}"
|
||||
|
||||
existing = session.exec(select(Product).where(Product.barcode == barcode)).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Barcode existiert bereits")
|
||||
|
||||
product = Product(**{**product_in.model_dump(), "barcode": barcode})
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@router.get("/search", response_model=list[ProductSearchResult])
|
||||
def search_products(
|
||||
current_user: CurrentUserDep,
|
||||
session: SessionDep,
|
||||
q: str = Query(default=""),
|
||||
limit: int = Query(default=20, ge=1, le=50),
|
||||
):
|
||||
query = select(Product)
|
||||
if q:
|
||||
query = query.where(Product.name.ilike(f"%{q}%"))
|
||||
products = [
|
||||
product
|
||||
for product in session.exec(query).all()
|
||||
if product_barcode_accessible(session, product.barcode, current_user.id)
|
||||
]
|
||||
|
||||
recent_logs = session.exec(
|
||||
select(FoodLog)
|
||||
.where(FoodLog.user_id == current_user.id)
|
||||
.order_by(FoodLog.timestamp.desc())
|
||||
.limit(500)
|
||||
).all()
|
||||
last_used: dict[str, FoodLog] = {}
|
||||
for log in recent_logs:
|
||||
if log.barcode and log.barcode not in last_used:
|
||||
last_used[log.barcode] = log
|
||||
|
||||
def sort_key(product: Product):
|
||||
log = last_used.get(product.barcode)
|
||||
if log:
|
||||
return (0, -log.timestamp.timestamp())
|
||||
return (1, product.name.lower())
|
||||
|
||||
products.sort(key=sort_key)
|
||||
|
||||
results = []
|
||||
for product in products[:limit]:
|
||||
log = last_used.get(product.barcode)
|
||||
results.append(
|
||||
ProductSearchResult(
|
||||
**product.model_dump(),
|
||||
last_amount_g=log.amount_g if log else None,
|
||||
is_dish=product.barcode.startswith("dish-"),
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/{barcode}", response_model=ProductRead)
|
||||
async def get_product(barcode: str, current_user: CurrentUserDep, session: SessionDep):
|
||||
product = session.exec(select(Product).where(Product.barcode == barcode)).first()
|
||||
if product:
|
||||
if not product_barcode_accessible(session, barcode, current_user.id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Produkt nicht gefunden")
|
||||
return product
|
||||
|
||||
product = await _fetch_from_open_food_facts(barcode)
|
||||
if product is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Produkt nicht gefunden")
|
||||
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
@@ -0,0 +1,31 @@
|
||||
from fastapi import APIRouter
|
||||
from sqlmodel import select
|
||||
|
||||
from app.deps import CurrentUserDep, SessionDep
|
||||
from app.models import User
|
||||
from app.schemas import GoalsUpdate, UserRead, UserSummary
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserSummary])
|
||||
def list_users(current_user: CurrentUserDep, session: SessionDep):
|
||||
users = session.exec(select(User).where(User.id != current_user.id).order_by(User.username)).all()
|
||||
return [UserSummary(username=user.username) for user in users]
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserRead)
|
||||
def read_current_user(current_user: CurrentUserDep):
|
||||
return current_user
|
||||
|
||||
|
||||
@router.put("/me/goals", response_model=UserRead)
|
||||
def update_goals(goals: GoalsUpdate, current_user: CurrentUserDep, session: SessionDep):
|
||||
current_user.calorie_goal = goals.calorie_goal
|
||||
current_user.protein_goal = goals.protein_goal
|
||||
current_user.carbs_goal = goals.carbs_goal
|
||||
current_user.fat_goal = goals.fat_goal
|
||||
session.add(current_user)
|
||||
session.commit()
|
||||
session.refresh(current_user)
|
||||
return current_user
|
||||
Reference in New Issue
Block a user