mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-26 12:15:19 -05:00
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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
8
mealie/routes/unit_and_foods/__init__.py
Normal file
8
mealie/routes/unit_and_foods/__init__.py
Normal 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)
|
||||
34
mealie/routes/unit_and_foods/food_routes.py
Normal file
34
mealie/routes/unit_and_foods/food_routes.py
Normal 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
|
||||
34
mealie/routes/unit_and_foods/unit_routes.py
Normal file
34
mealie/routes/unit_and_foods/unit_routes.py
Normal 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
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user