Add Database Layer for Recipe Scaling (#506)

* move badge

* fix add individual ingredient

* fix redirect issue

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-06-12 22:23:23 -08:00
committed by GitHub
parent 0a927afaa0
commit e95ca870b1
18 changed files with 298 additions and 62 deletions

View File

@@ -6,6 +6,7 @@ from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlan
from mealie.db.models.recipe.comment import RecipeComment
from mealie.db.models.recipe.ingredient import IngredientFood, IngredientUnit
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
from mealie.db.models.recipe.settings import RecipeSettings
from mealie.db.models.settings import CustomPage, SiteSettings
@@ -18,7 +19,8 @@ from mealie.schema.comments import CommentOut
from mealie.schema.event_notifications import EventNotificationIn
from mealie.schema.events import Event as EventSchema
from mealie.schema.meal import MealPlanOut
from mealie.schema.recipe import Recipe
from mealie.schema.recipe import (Recipe, RecipeIngredientFood,
RecipeIngredientUnit)
from mealie.schema.settings import CustomPageOut
from mealie.schema.settings import SiteSettings as SiteSettingsSchema
from mealie.schema.shopping_list import ShoppingListOut
@@ -87,6 +89,20 @@ class _Recipes(BaseDocument):
)
class _IngredientFoods(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
self.sql_model = IngredientFood
self.schema = RecipeIngredientFood
class _IngredientUnits(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
self.sql_model = IngredientUnit
self.schema = RecipeIngredientUnit
class _Categories(BaseDocument):
def __init__(self) -> None:
self.primary_key = "slug"
@@ -215,21 +231,28 @@ class _EventNotification(BaseDocument):
class Database:
def __init__(self) -> None:
# Recipes
self.recipes = _Recipes()
self.meals = _Meals()
self.settings = _Settings()
self.themes = _Themes()
self.ingredient_foods = _IngredientUnits()
self.ingredient_units = _IngredientFoods()
self.categories = _Categories()
self.tags = _Tags()
self.comments = _Comments()
# Site
self.settings = _Settings()
self.themes = _Themes()
self.sign_ups = _SignUps()
self.custom_pages = _CustomPages()
self.event_notifications = _EventNotification()
self.events = _Events()
# Users / Groups
self.users = _Users()
self.api_tokens = _LongLiveToken()
self.sign_ups = _SignUps()
self.groups = _Groups()
self.custom_pages = _CustomPages()
self.events = _Events()
self.event_notifications = _EventNotification()
self.meals = _Meals()
self.shopping_lists = _ShoppingList()
self.comments = _Comments()
db = Database()

View File

@@ -1,5 +1,70 @@
from mealie.db.models.model_base import SqlAlchemyBase
from sqlalchemy import Column, ForeignKey, Integer, String
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from requests import Session
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table, orm
ingredients_to_units = Table(
"ingredients_to_units",
SqlAlchemyBase.metadata,
Column("ingredient_units.id", Integer, ForeignKey("ingredient_units.id")),
Column("recipes_ingredients_id", Integer, ForeignKey("recipes_ingredients.id")),
)
ingredients_to_foods = Table(
"ingredients_to_foods",
SqlAlchemyBase.metadata,
Column("ingredient_foods.id", Integer, ForeignKey("ingredient_foods.id")),
Column("recipes_ingredients_id", Integer, ForeignKey("recipes_ingredients.id")),
)
class IngredientUnit(SqlAlchemyBase, BaseMixins):
__tablename__ = "ingredient_units"
id = Column(Integer, primary_key=True)
name = Column(String)
description = Column(String)
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_units, back_populates="unit")
def __init__(self, name: str, description: str = None) -> None:
self.name = name
self.description = description
@classmethod
def get_ref_or_create(cls, session: Session, obj: dict):
# sourcery skip: flip-comparison
if obj is None:
return None
name = obj.get("name")
unit = session.query(cls).filter("name" == name).one_or_none()
if not unit:
return cls(**obj)
class IngredientFood(SqlAlchemyBase, BaseMixins):
__tablename__ = "ingredient_foods"
id = Column(Integer, primary_key=True)
name = Column(String)
description = Column(String)
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_foods, back_populates="food")
def __init__(self, name: str, description: str = None) -> None:
self.name = name
self.description = description
@classmethod
def get_ref_or_create(cls, session: Session, obj: dict):
# sourcery skip: flip-comparison
if obj is None:
return None
name = obj.get("name")
unit = session.query(cls).filter("name" == name).one_or_none()
if not unit:
return cls(**obj)
class RecipeIngredient(SqlAlchemyBase):
@@ -7,8 +72,24 @@ class RecipeIngredient(SqlAlchemyBase):
id = Column(Integer, primary_key=True)
position = Column(Integer)
parent_id = Column(Integer, ForeignKey("recipes.id"))
# title = Column(String)
ingredient = Column(String)
def update(self, ingredient):
self.ingredient = ingredient
title = Column(String) # Section Header - Shows if Present
note = Column(String) # Force Show Text - Overrides Concat
# Scaling Items
unit = orm.relationship(IngredientUnit, secondary=ingredients_to_units, uselist=False)
food = orm.relationship(IngredientFood, secondary=ingredients_to_foods, uselist=False)
quantity = Column(Integer)
# Extras
disable_amount = Column(Boolean, default=False)
def __init__(
self, title: str, note: str, unit: dict, food: dict, quantity: int, disable_amount: bool, session: Session, **_
) -> None:
self.title = title
self.note = note
self.unit = IngredientUnit.get_ref_or_create(session, unit)
self.food = IngredientFood.get_ref_or_create(session, food)
self.quantity = quantity
self.disable_amount = disable_amount

View File

@@ -117,7 +117,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
self.tools = [Tool(tool=x) for x in tools] if tools else []
self.recipe_yield = recipe_yield
self.recipe_ingredient = [RecipeIngredient(ingredient=ingr) for ingr in recipe_ingredient]
self.recipe_ingredient = [RecipeIngredient(**ingr, session=session) for ingr in recipe_ingredient]
self.assets = [RecipeAsset(**a) for a in assets]
self.recipe_instructions = [
RecipeInstruction(text=instruc.get("text"), title=instruc.get("title"), type=instruc.get("@type", None))

View File

@@ -37,7 +37,7 @@ def get_shopping_list(
logger.error("Recipe Not Found")
new_list = ShoppingListIn(
name="MealPlan Shopping List", group=current_user.group, items=[ListItem(text=t) for t in all_ingredients]
name="MealPlan Shopping List", group=current_user.group, items=[ListItem(text=t.note) for t in all_ingredients]
)
created_list: ShoppingListOut = db.shopping_lists.create(session, new_list)

View File

@@ -76,6 +76,9 @@ def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), i
recipe: Recipe = db.recipes.get(session, recipe_slug)
if not recipe:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if recipe.settings.public or is_user:
return recipe

View File

@@ -0,0 +1,8 @@
from fastapi import APIRouter
from . import food_routes, unit_routes
units_and_foods_router = APIRouter(tags=["Food and Units"])
units_and_foods_router.include_router(food_routes.router)
units_and_foods_router.include_router(unit_routes.router)

View File

@@ -0,0 +1,34 @@
from fastapi import APIRouter, Depends
from mealie.core.root_logger import get_logger
from mealie.routes.deps import get_current_user
router = APIRouter(prefix="/api/foods", dependencies=[Depends(get_current_user)])
logger = get_logger()
@router.post("")
async def create_food():
""" Create food in the Database """
# Create food
pass
@router.get("/{id}")
async def get_food():
""" Get food from the Database """
# Get food
pass
@router.put("/{id}")
async def update_food():
""" Update food in the Database """
# Update food
pass
@router.delete("/{id}")
async def delete_food():
""" Delete food from the Database """
# Delete food
pass

View File

@@ -0,0 +1,34 @@
from fastapi import APIRouter, Depends
from mealie.core.root_logger import get_logger
from mealie.routes.deps import get_current_user
router = APIRouter(prefix="/api/units", dependencies=[Depends(get_current_user)])
logger = get_logger()
@router.post("")
async def create_food():
""" Create food in the Database """
# Create food
pass
@router.get("/{id}")
async def get_food():
""" Get food from the Database """
# Get food
pass
@router.put("/{id}")
async def update_food():
""" Update food in the Database """
# Update food
pass
@router.delete("/{id}")
async def delete_food():
""" Delete food from the Database """
# Delete food
pass

View File

@@ -59,6 +59,30 @@ class Nutrition(CamelModel):
orm_mode = True
class RecipeIngredientFood(CamelModel):
name: str = ""
description: str = ""
class Config:
orm_mode = True
class RecipeIngredientUnit(RecipeIngredientFood):
pass
class RecipeIngredient(CamelModel):
title: Optional[str]
note: Optional[str]
unit: Optional[RecipeIngredientUnit]
food: Optional[RecipeIngredientFood]
disable_amount: bool = True
quantity: int = 1
class Config:
orm_mode = True
class RecipeSummary(CamelModel):
id: Optional[int]
name: Optional[str]
@@ -87,7 +111,7 @@ class RecipeSummary(CamelModel):
class Recipe(RecipeSummary):
recipe_yield: Optional[str]
recipe_ingredient: Optional[list[str]]
recipe_ingredient: Optional[list[RecipeIngredient]]
recipe_instructions: Optional[list[RecipeStep]]
nutrition: Optional[Nutrition]
tools: Optional[list[str]] = []
@@ -134,7 +158,7 @@ class Recipe(RecipeSummary):
def getter_dict(_cls, name_orm: RecipeModel):
return {
**GetterDict(name_orm),
"recipe_ingredient": [x.ingredient for x in name_orm.recipe_ingredient],
# "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient],
"recipe_category": [x.name for x in name_orm.recipe_category],
"tags": [x.name for x in name_orm.tags],
"tools": [x.tool for x in name_orm.tools],
@@ -179,6 +203,16 @@ class Recipe(RecipeSummary):
return slug
@validator("recipe_ingredient", always=True, pre=True)
def validate_ingredients(recipe_ingredient, values):
if not recipe_ingredient or not isinstance(recipe_ingredient, list):
return recipe_ingredient
if all(isinstance(elem, str) for elem in recipe_ingredient):
return [RecipeIngredient(note=x) for x in recipe_ingredient]
return recipe_ingredient
class AllRecipeRequest(BaseModel):
properties: list[str]