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:
Hayden
2021-04-28 18:59:37 -08:00
committed by GitHub
parent 9abb6f10fd
commit 04255e285f
25 changed files with 246 additions and 749 deletions

View File

@@ -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

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View 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

View 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")

View 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),

View File

@@ -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: