mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-03 23:51:22 -05:00
feature/new-recipe-features (#360)
* unify button styles * fix drag on mobile * recipe instructions section * add carbs * refactor component location * asset start * consolidate view/edit components * asset api * base dialog event * Remove 'content' * remove console.log * add slug prop * remove console.log * recipe assets first pass * add recipeSettings model * fix hide/show when no tags/categories * fix typo Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -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
|
||||
|
||||
22
mealie/db/models/recipe/assets.py
Normal file
22
mealie/db/models/recipe/assets.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import sqlalchemy as sa
|
||||
from mealie.db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class RecipeAsset(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_assets"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
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
|
||||
@@ -9,3 +9,4 @@ class RecipeInstruction(SqlAlchemyBase):
|
||||
position = sa.Column(sa.Integer)
|
||||
type = sa.Column(sa.String, default="")
|
||||
text = sa.Column(sa.String)
|
||||
title = sa.Column(sa.String)
|
||||
|
||||
@@ -10,6 +10,7 @@ class Nutrition(SqlAlchemyBase):
|
||||
fatContent = sa.Column(sa.String)
|
||||
fiberContent = sa.Column(sa.String)
|
||||
proteinContent = sa.Column(sa.String)
|
||||
carbohydrateContent = sa.Column(sa.String)
|
||||
sodiumContent = sa.Column(sa.String)
|
||||
sugarContent = sa.Column(sa.String)
|
||||
|
||||
@@ -21,6 +22,7 @@ class Nutrition(SqlAlchemyBase):
|
||||
proteinContent=None,
|
||||
sodiumContent=None,
|
||||
sugarContent=None,
|
||||
carbohydrateContent=None,
|
||||
) -> None:
|
||||
self.calories = calories
|
||||
self.fatContent = fatContent
|
||||
@@ -28,3 +30,4 @@ class Nutrition(SqlAlchemyBase):
|
||||
self.proteinContent = proteinContent
|
||||
self.sodiumContent = sodiumContent
|
||||
self.sugarContent = sugarContent
|
||||
self.carbohydrateContent = carbohydrateContent
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import datetime
|
||||
from datetime import date
|
||||
from typing import List
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.recipe.api_extras import ApiExtras
|
||||
from mealie.db.models.recipe.assets import RecipeAsset
|
||||
from mealie.db.models.recipe.category import Category, recipes2categories
|
||||
from mealie.db.models.recipe.ingredient import RecipeIngredient
|
||||
from mealie.db.models.recipe.instruction import RecipeInstruction
|
||||
from mealie.db.models.recipe.note import Note
|
||||
from mealie.db.models.recipe.nutrition import Nutrition
|
||||
from mealie.db.models.recipe.settings import RecipeSettings
|
||||
from mealie.db.models.recipe.tag import Tag, recipes2tags
|
||||
from mealie.db.models.recipe.tool import Tool
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
@@ -32,17 +33,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",
|
||||
@@ -51,12 +53,13 @@ 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")
|
||||
settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan")
|
||||
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):
|
||||
@@ -70,22 +73,24 @@ 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,
|
||||
assets: list = None,
|
||||
settings: dict = None,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None:
|
||||
@@ -95,12 +100,14 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
self.recipeCuisine = recipeCuisine
|
||||
|
||||
self.nutrition = Nutrition(**nutrition) if self.nutrition else Nutrition()
|
||||
|
||||
self.tools = [Tool(tool=x) for x in tools] if tools else []
|
||||
|
||||
self.recipeYield = recipeYield
|
||||
self.recipeIngredient = [RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient]
|
||||
self.assets = [RecipeAsset(**a) for a in assets]
|
||||
self.recipeInstructions = [
|
||||
RecipeInstruction(text=instruc.get("text"), type=instruc.get("@type", None))
|
||||
RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None))
|
||||
for instruc in recipeInstructions
|
||||
]
|
||||
self.totalTime = totalTime
|
||||
@@ -110,6 +117,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
self.recipeCategory = [Category.create_if_not_exist(session=session, name=cat) for cat in recipeCategory]
|
||||
|
||||
# Mealie Specific
|
||||
self.settings = RecipeSettings(**settings) if settings else RecipeSettings()
|
||||
print(self.settings)
|
||||
self.tags = [Tag.create_if_not_exist(session=session, name=tag) for tag in tags]
|
||||
self.slug = slug
|
||||
self.dateAdded = dateAdded
|
||||
@@ -118,54 +127,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
self.orgURL = orgURL
|
||||
self.extras = [ApiExtras(key=key, value=value) for key, value in extras.items()]
|
||||
|
||||
def update(
|
||||
self,
|
||||
session,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
image: str = None,
|
||||
recipeYield: str = None,
|
||||
recipeIngredient: List[str] = None,
|
||||
recipeInstructions: List[dict] = None,
|
||||
recipeCuisine: str = None,
|
||||
totalTime: str = None,
|
||||
tools: list[str] = [],
|
||||
prepTime: str = None,
|
||||
performTime: str = None,
|
||||
nutrition: dict = None,
|
||||
slug: str = None,
|
||||
recipeCategory: List[str] = None,
|
||||
tags: List[str] = None,
|
||||
dateAdded: datetime.date = None,
|
||||
notes: List[dict] = None,
|
||||
rating: int = None,
|
||||
orgURL: str = None,
|
||||
extras: dict = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
def update(self, *args, **kwargs):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
|
||||
self.__init__(
|
||||
session=session,
|
||||
name=name,
|
||||
description=description,
|
||||
image=image,
|
||||
recipeYield=recipeYield,
|
||||
recipeIngredient=recipeIngredient,
|
||||
recipeInstructions=recipeInstructions,
|
||||
totalTime=totalTime,
|
||||
recipeCuisine=recipeCuisine,
|
||||
prepTime=prepTime,
|
||||
performTime=performTime,
|
||||
nutrition=nutrition,
|
||||
tools=tools,
|
||||
slug=slug,
|
||||
recipeCategory=recipeCategory,
|
||||
tags=tags,
|
||||
dateAdded=dateAdded,
|
||||
notes=notes,
|
||||
rating=rating,
|
||||
orgURL=orgURL,
|
||||
extras=extras,
|
||||
)
|
||||
self.__init__(*args, **kwargs)
|
||||
|
||||
18
mealie/db/models/recipe/settings.py
Normal file
18
mealie/db/models/recipe/settings.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import sqlalchemy as sa
|
||||
from mealie.db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class RecipeSettings(SqlAlchemyBase):
|
||||
__tablename__ = "recipe_settings"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.Integer, sa.ForeignKey("recipes.id"))
|
||||
public = sa.Column(sa.Boolean)
|
||||
show_nutrition = sa.Column(sa.Boolean)
|
||||
show_assets = sa.Column(sa.Boolean)
|
||||
landscape_view = sa.Column(sa.Boolean)
|
||||
|
||||
def __init__(self, public=True, show_nutrition=True, show_assets=True, landscape_view=True) -> None:
|
||||
self.public = public
|
||||
self.show_nutrition = show_nutrition
|
||||
self.show_assets = show_assets
|
||||
self.landscape_view = landscape_view
|
||||
50
mealie/routes/recipe/recipe_assets.py
Normal file
50
mealie/routes/recipe/recipe_assets.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form
|
||||
from fastapi.datastructures import UploadFile
|
||||
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")
|
||||
@@ -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),
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import datetime
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from pydantic import BaseModel, validator
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic.utils import GetterDict
|
||||
from slugify import slugify
|
||||
|
||||
|
||||
class RecipeSettings(CamelModel):
|
||||
public: bool = True
|
||||
show_nutrition: bool = True
|
||||
show_assets: bool = True
|
||||
landscape_view: bool = True
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeNote(BaseModel):
|
||||
title: str
|
||||
text: str
|
||||
@@ -15,18 +26,29 @@ class RecipeNote(BaseModel):
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeStep(BaseModel):
|
||||
class RecipeStep(CamelModel):
|
||||
title: Optional[str] = ""
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeAsset(CamelModel):
|
||||
name: str
|
||||
icon: str
|
||||
file_name: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class Nutrition(BaseModel):
|
||||
calories: Optional[str]
|
||||
fatContent: Optional[str]
|
||||
fiberContent: Optional[str]
|
||||
proteinContent: Optional[str]
|
||||
carbohydrateContent: Optional[str]
|
||||
fiberContent: Optional[str]
|
||||
sodiumContent: Optional[str]
|
||||
sugarContent: Optional[str]
|
||||
|
||||
@@ -41,8 +63,8 @@ class RecipeSummary(BaseModel):
|
||||
image: Optional[Any]
|
||||
|
||||
description: Optional[str]
|
||||
recipeCategory: Optional[List[str]] = []
|
||||
tags: Optional[List[str]] = []
|
||||
recipeCategory: Optional[list[str]] = []
|
||||
tags: Optional[list[str]] = []
|
||||
rating: Optional[int]
|
||||
|
||||
class Config:
|
||||
@@ -69,8 +91,10 @@ class Recipe(RecipeSummary):
|
||||
performTime: Optional[str] = None
|
||||
|
||||
# Mealie Specific
|
||||
settings: Optional[RecipeSettings]
|
||||
assets: Optional[list[RecipeAsset]] = []
|
||||
dateAdded: Optional[datetime.date]
|
||||
notes: Optional[List[RecipeNote]] = []
|
||||
notes: Optional[list[RecipeNote]] = []
|
||||
orgURL: Optional[str]
|
||||
extras: Optional[dict] = {}
|
||||
|
||||
@@ -126,7 +150,7 @@ class Recipe(RecipeSummary):
|
||||
|
||||
|
||||
class AllRecipeRequest(BaseModel):
|
||||
properties: List[str]
|
||||
properties: list[str]
|
||||
limit: Optional[int]
|
||||
|
||||
class Config:
|
||||
|
||||
Reference in New Issue
Block a user