diff --git a/.gitignore b/.gitignore index 3b2646bfc..b0c415482 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ scratch.py dev/data/backups/dev_sample_data*.zip dev/data/backups/dev_sample_data*.zip !dev/data/backups/test*.zip +dev/data/recipes/* diff --git a/mealie/app.py b/mealie/app.py index 0d16c615b..13d325cec 100644 --- a/mealie/app.py +++ b/mealie/app.py @@ -8,7 +8,7 @@ from mealie.core.config import APP_VERSION, settings from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes from mealie.routes.groups import groups from mealie.routes.mealplans import mealplans -from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes +from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_assets, recipe_crud_routes, tag_routes from mealie.routes.site_settings import all_settings from mealie.routes.users import users @@ -37,6 +37,7 @@ def api_routers(): app.include_router(category_routes.router) app.include_router(tag_routes.router) app.include_router(recipe_crud_routes.router) + app.include_router(recipe_assets.router) # Meal Routes app.include_router(mealplans.router) # Settings Routes diff --git a/mealie/db/models/recipe/assets.py b/mealie/db/models/recipe/assets.py index d1bbc7fbd..2dba97359 100644 --- a/mealie/db/models/recipe/assets.py +++ b/mealie/db/models/recipe/assets.py @@ -8,13 +8,15 @@ class RecipeAsset(SqlAlchemyBase): parent_id = sa.Column(sa.String, sa.ForeignKey("recipes.id")) name = sa.Column(sa.String) icon = sa.Column(sa.String) - + file_name = sa.Column(sa.String) def __init__( self, name=None, icon=None, + file_name=None, ) -> None: + print("Asset Saved", name) self.name = name + self.file_name = file_name self.icon = icon - diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py index 686c84bc4..d827c9025 100644 --- a/mealie/db/models/recipe/recipe.py +++ b/mealie/db/models/recipe/recipe.py @@ -1,6 +1,5 @@ import datetime from datetime import date -from typing import List import sqlalchemy as sa import sqlalchemy.orm as orm @@ -33,17 +32,18 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): cookTime = sa.Column(sa.String) recipeYield = sa.Column(sa.String) recipeCuisine = sa.Column(sa.String) - tools: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan") + tools: list[Tool] = orm.relationship("Tool", cascade="all, delete-orphan") + assets: list[RecipeAsset] = orm.relationship("RecipeAsset", cascade="all, delete-orphan") nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan") - recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes") + recipeCategory: list = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes") - recipeIngredient: List[RecipeIngredient] = orm.relationship( + recipeIngredient: list[RecipeIngredient] = orm.relationship( "RecipeIngredient", cascade="all, delete-orphan", order_by="RecipeIngredient.position", collection_class=ordering_list("position"), ) - recipeInstructions: List[RecipeInstruction] = orm.relationship( + recipeInstructions: list[RecipeInstruction] = orm.relationship( "RecipeInstruction", cascade="all, delete-orphan", order_by="RecipeInstruction.position", @@ -52,12 +52,12 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): # Mealie Specific slug = sa.Column(sa.String, index=True, unique=True) - tags: List[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes") + tags: list[Tag] = orm.relationship("Tag", secondary=recipes2tags, back_populates="recipes") dateAdded = sa.Column(sa.Date, default=date.today) - notes: List[Note] = orm.relationship("Note", cascade="all, delete-orphan") + notes: list[Note] = orm.relationship("Note", cascade="all, delete-orphan") rating = sa.Column(sa.Integer) orgURL = sa.Column(sa.String) - extras: List[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan") + extras: list[ApiExtras] = orm.relationship("ApiExtras", cascade="all, delete-orphan") @validates("name") def validate_name(self, key, name): @@ -71,19 +71,19 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): description: str = None, image: str = None, recipeYield: str = None, - recipeIngredient: List[str] = None, - recipeInstructions: List[dict] = None, + recipeIngredient: list[str] = None, + recipeInstructions: list[dict] = None, recipeCuisine: str = None, totalTime: str = None, prepTime: str = None, nutrition: dict = None, - tools: list[str] = [], + tools: list[str] = None, performTime: str = None, slug: str = None, - recipeCategory: List[str] = None, - tags: List[str] = None, + recipeCategory: list[str] = None, + tags: list[str] = None, dateAdded: datetime.date = None, - notes: List[dict] = None, + notes: list[dict] = None, rating: int = None, orgURL: str = None, extras: dict = None, @@ -101,7 +101,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): self.recipeYield = recipeYield self.recipeIngredient = [RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient] - self.assets = [RecipeAsset(name=a.get("name"), icon=a.get("icon")) for a in assets] + self.assets = [RecipeAsset(**a) for a in assets] self.recipeInstructions = [ RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None)) for instruc in recipeInstructions diff --git a/mealie/routes/recipe/recipe_assets.py b/mealie/routes/recipe/recipe_assets.py new file mode 100644 index 000000000..f8fa4bfd4 --- /dev/null +++ b/mealie/routes/recipe/recipe_assets.py @@ -0,0 +1,52 @@ +import shutil + +from fastapi import APIRouter, Depends, File, Form +from fastapi.datastructures import UploadFile +from fastapi.routing import run_endpoint_function +from mealie.core.config import app_dirs +from mealie.db.database import db +from mealie.db.db_setup import generate_session +from mealie.routes.deps import get_current_user +from mealie.schema.recipe import Recipe, RecipeAsset +from mealie.schema.snackbar import SnackResponse +from slugify import slugify +from sqlalchemy.orm.session import Session +from starlette.responses import FileResponse + +router = APIRouter(prefix="/api/recipes", tags=["Recipe Assets"]) + + +@router.get("/{recipe_slug}/asset") +async def get_recipe_asset(recipe_slug, file_name: str): + """ Returns a recipe asset """ + file = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name) + + return FileResponse(file) + + +@router.post("/{recipe_slug}/asset", response_model=RecipeAsset) +def upload_recipe_asset( + recipe_slug: str, + name: str = Form(...), + icon: str = Form(...), + extension: str = Form(...), + file: UploadFile = File(...), + session: Session = Depends(generate_session), + current_user=Depends(get_current_user), +): + """ Upload a file to store as a recipe asset """ + file_name = slugify(name) + "." + extension + asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) + dest = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name) + dest.parent.mkdir(exist_ok=True, parents=True) + + with dest.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + if dest.is_file(): + recipe: Recipe = db.recipes.get(session, recipe_slug) + recipe.assets.append(asset_in) + db.recipes.update(session, recipe_slug, recipe.dict()) + return asset_in + else: + return SnackResponse.error("Failure uploading file") diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 1ee6a3e25..4b7f6f322 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -57,6 +57,7 @@ def update_recipe( """ Updates a recipe by existing slug and data. """ recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict()) + print(recipe.assets) if recipe_slug != recipe.slug: rename_image(original_slug=recipe_slug, new_slug=recipe.slug) @@ -65,7 +66,7 @@ def update_recipe( @router.patch("/{recipe_slug}") -def update_recipe( +def patch_recipe( recipe_slug: str, data: dict, session: Session = Depends(generate_session), diff --git a/mealie/schema/recipe.py b/mealie/schema/recipe.py index e0c582d97..db851cdba 100644 --- a/mealie/schema/recipe.py +++ b/mealie/schema/recipe.py @@ -1,6 +1,7 @@ import datetime from typing import Any, List, Optional +from fastapi_camelcase import CamelModel from mealie.db.models.recipe.recipe import RecipeModel from pydantic import BaseModel, validator from pydantic.utils import GetterDict @@ -15,16 +16,18 @@ class RecipeNote(BaseModel): orm_mode = True -class RecipeStep(BaseModel): +class RecipeStep(CamelModel): title: Optional[str] = "" text: str class Config: orm_mode = True -class RecipeAsset(BaseModel): + +class RecipeAsset(CamelModel): name: str icon: str + file_name: Optional[str] class Config: orm_mode = True