165 lines
5.4 KiB
Python
165 lines
5.4 KiB
Python
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
|
from slugify import slugify
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import UPLOAD_DIR, get_db
|
|
from app.models import Ingredient, Recipe, Step
|
|
from app.schemas import RecipeIn, RecipeOut, RecipeSummary
|
|
|
|
router = APIRouter(prefix="/api/recipes", tags=["recipes"])
|
|
|
|
ALLOWED_IMAGE_TYPES = {"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}
|
|
|
|
|
|
def _unique_slug(db: Session, name: str, *, exclude_id: int | None = None) -> str:
|
|
base = slugify(name) or "recipe"
|
|
slug = base
|
|
suffix = 2
|
|
while True:
|
|
stmt = select(Recipe.id).where(Recipe.slug == slug)
|
|
if exclude_id is not None:
|
|
stmt = stmt.where(Recipe.id != exclude_id)
|
|
if db.scalar(stmt) is None:
|
|
return slug
|
|
slug = f"{base}-{suffix}"
|
|
suffix += 1
|
|
|
|
|
|
def _apply_children(db: Session, recipe: Recipe, payload: RecipeIn) -> None:
|
|
recipe.ingredients.clear()
|
|
for position, ing in enumerate(payload.ingredients):
|
|
recipe.ingredients.append(
|
|
Ingredient(
|
|
position=position,
|
|
kind=ing.kind,
|
|
quantity=ing.quantity,
|
|
unit=ing.unit,
|
|
text=ing.text,
|
|
)
|
|
)
|
|
recipe.steps.clear()
|
|
for position, step in enumerate(payload.steps):
|
|
recipe.steps.append(Step(position=position, text=step.text))
|
|
|
|
|
|
@router.get("", response_model=list[RecipeSummary])
|
|
def list_recipes(search: str | None = None, db: Session = Depends(get_db)):
|
|
stmt = select(Recipe).order_by(Recipe.name)
|
|
if search:
|
|
like = f"%{search.lower()}%"
|
|
stmt = stmt.where(
|
|
Recipe.name.ilike(like) | Recipe.category.ilike(like)
|
|
)
|
|
return db.scalars(stmt).all()
|
|
|
|
|
|
@router.get("/{recipe_id}", response_model=RecipeOut)
|
|
def get_recipe(recipe_id: int, db: Session = Depends(get_db)):
|
|
recipe = db.get(Recipe, recipe_id)
|
|
if recipe is None:
|
|
raise HTTPException(status_code=404, detail="Recipe not found")
|
|
return recipe
|
|
|
|
|
|
@router.post("", response_model=RecipeOut, status_code=201)
|
|
def create_recipe(payload: RecipeIn, db: Session = Depends(get_db)):
|
|
recipe = Recipe(
|
|
name=payload.name,
|
|
slug=_unique_slug(db, payload.name),
|
|
category=payload.category,
|
|
preptime=payload.preptime,
|
|
cooktime=payload.cooktime,
|
|
baketime=payload.baketime,
|
|
cooltime=payload.cooltime,
|
|
marinade_time=payload.marinade_time,
|
|
robot_time=payload.robot_time,
|
|
servings=payload.servings,
|
|
notes=payload.notes,
|
|
kcal=payload.kcal,
|
|
kh=payload.kh,
|
|
protein=payload.protein,
|
|
fett=payload.fett,
|
|
)
|
|
_apply_children(db, recipe, payload)
|
|
db.add(recipe)
|
|
db.commit()
|
|
db.refresh(recipe)
|
|
return recipe
|
|
|
|
|
|
@router.put("/{recipe_id}", response_model=RecipeOut)
|
|
def update_recipe(recipe_id: int, payload: RecipeIn, db: Session = Depends(get_db)):
|
|
recipe = db.get(Recipe, recipe_id)
|
|
if recipe is None:
|
|
raise HTTPException(status_code=404, detail="Recipe not found")
|
|
|
|
if payload.name != recipe.name:
|
|
recipe.slug = _unique_slug(db, payload.name, exclude_id=recipe_id)
|
|
recipe.name = payload.name
|
|
recipe.category = payload.category
|
|
recipe.preptime = payload.preptime
|
|
recipe.cooktime = payload.cooktime
|
|
recipe.baketime = payload.baketime
|
|
recipe.cooltime = payload.cooltime
|
|
recipe.marinade_time = payload.marinade_time
|
|
recipe.robot_time = payload.robot_time
|
|
recipe.servings = payload.servings
|
|
recipe.notes = payload.notes
|
|
recipe.kcal = payload.kcal
|
|
recipe.kh = payload.kh
|
|
recipe.protein = payload.protein
|
|
recipe.fett = payload.fett
|
|
_apply_children(db, recipe, payload)
|
|
|
|
db.commit()
|
|
db.refresh(recipe)
|
|
return recipe
|
|
|
|
|
|
@router.delete("/{recipe_id}", status_code=204)
|
|
def delete_recipe(recipe_id: int, db: Session = Depends(get_db)):
|
|
recipe = db.get(Recipe, recipe_id)
|
|
if recipe is None:
|
|
raise HTTPException(status_code=404, detail="Recipe not found")
|
|
if recipe.image_filename:
|
|
(UPLOAD_DIR / recipe.image_filename).unlink(missing_ok=True)
|
|
db.delete(recipe)
|
|
db.commit()
|
|
|
|
|
|
@router.post("/{recipe_id}/image", response_model=RecipeOut)
|
|
async def upload_recipe_image(recipe_id: int, file: UploadFile, db: Session = Depends(get_db)):
|
|
recipe = db.get(Recipe, recipe_id)
|
|
if recipe is None:
|
|
raise HTTPException(status_code=404, detail="Recipe not found")
|
|
ext = ALLOWED_IMAGE_TYPES.get(file.content_type or "")
|
|
if ext is None:
|
|
raise HTTPException(status_code=400, detail="Only JPEG, PNG or WEBP images are allowed")
|
|
|
|
if recipe.image_filename:
|
|
(UPLOAD_DIR / recipe.image_filename).unlink(missing_ok=True)
|
|
|
|
filename = f"{uuid.uuid4().hex}{ext}"
|
|
dest = UPLOAD_DIR / filename
|
|
dest.write_bytes(await file.read())
|
|
|
|
recipe.image_filename = filename
|
|
db.commit()
|
|
db.refresh(recipe)
|
|
return recipe
|
|
|
|
|
|
@router.delete("/{recipe_id}/image", response_model=RecipeOut)
|
|
def delete_recipe_image(recipe_id: int, db: Session = Depends(get_db)):
|
|
recipe = db.get(Recipe, recipe_id)
|
|
if recipe is None:
|
|
raise HTTPException(status_code=404, detail="Recipe not found")
|
|
if recipe.image_filename:
|
|
(UPLOAD_DIR / recipe.image_filename).unlink(missing_ok=True)
|
|
recipe.image_filename = None
|
|
db.commit()
|
|
db.refresh(recipe)
|
|
return recipe
|