mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat(backend): 🗃️ Add CRUD opertaions for Food and Units
This commit is contained in:
parent
c894d3d880
commit
122d35ec09
0
mealie/db/data_initialization/__init__.py
Normal file
0
mealie/db/data_initialization/__init__.py
Normal file
34
mealie/db/data_initialization/init_units_foods.py
Normal file
34
mealie/db/data_initialization/init_units_foods.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from mealie.schema.recipe.recipe import IngredientUnit
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
from ..data_access_layer import DatabaseAccessLayer
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_units():
|
||||||
|
return [
|
||||||
|
# Volume
|
||||||
|
IngredientUnit(name="teaspoon", abbreviation="tsp"),
|
||||||
|
IngredientUnit(name="tablespoon", abbreviation="tbsp"),
|
||||||
|
IngredientUnit(name="fluid ounce", abbreviation="fl oz"),
|
||||||
|
IngredientUnit(name="cup", abbreviation="cup"),
|
||||||
|
IngredientUnit(name="pint", abbreviation="pt"),
|
||||||
|
IngredientUnit(name="quart", abbreviation="qt"),
|
||||||
|
IngredientUnit(name="gallon", abbreviation="gal"),
|
||||||
|
IngredientUnit(name="milliliter", abbreviation="ml"),
|
||||||
|
IngredientUnit(name="liter", abbreviation="l"),
|
||||||
|
# Mass Weight
|
||||||
|
IngredientUnit(name="pound", abbreviation="lb"),
|
||||||
|
IngredientUnit(name="ounce", abbreviation="oz"),
|
||||||
|
IngredientUnit(name="gram", abbreviation="g"),
|
||||||
|
IngredientUnit(name="kilogram", abbreviation="kg"),
|
||||||
|
IngredientUnit(name="milligram", abbreviation="mg"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def default_recipe_unit_init(db: DatabaseAccessLayer, session: Session) -> None:
|
||||||
|
for unit in get_default_units():
|
||||||
|
try:
|
||||||
|
db.ingredient_units.create(session, unit)
|
||||||
|
print("Ingredient Unit Committed")
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
@ -1,9 +1,9 @@
|
|||||||
from mealie.db.models.event import *
|
from .event import *
|
||||||
from mealie.db.models.group import *
|
from .group import *
|
||||||
from mealie.db.models.mealplan import *
|
from .mealplan import *
|
||||||
from mealie.db.models.recipe.recipe import *
|
from .recipe.recipe import *
|
||||||
from mealie.db.models.settings import *
|
from .settings import *
|
||||||
from mealie.db.models.shopping_list import *
|
from .shopping_list import *
|
||||||
from mealie.db.models.sign_up import *
|
from .sign_up import *
|
||||||
from mealie.db.models.theme import *
|
from .theme import *
|
||||||
from mealie.db.models.users import *
|
from .users import *
|
||||||
|
112
mealie/db/models/_model_utils.py
Normal file
112
mealie/db/models/_model_utils.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY
|
||||||
|
|
||||||
|
|
||||||
|
def handle_one_to_many_list(relation_cls, all_elements: list[dict]):
|
||||||
|
elems_to_create = []
|
||||||
|
updated_elems = []
|
||||||
|
|
||||||
|
for elem in all_elements:
|
||||||
|
elem_id = elem.get("id", None)
|
||||||
|
|
||||||
|
existing_elem = relation_cls.get_ref(match_value=elem_id)
|
||||||
|
|
||||||
|
if existing_elem is None:
|
||||||
|
|
||||||
|
elems_to_create.append(elem)
|
||||||
|
|
||||||
|
else:
|
||||||
|
for key, value in elem.items():
|
||||||
|
setattr(existing_elem, key, value)
|
||||||
|
|
||||||
|
updated_elems.append(existing_elem)
|
||||||
|
|
||||||
|
new_elems = []
|
||||||
|
for elem in elems_to_create:
|
||||||
|
new_elems = [relation_cls(**elem) for elem in all_elements]
|
||||||
|
|
||||||
|
return new_elems
|
||||||
|
|
||||||
|
|
||||||
|
def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
|
||||||
|
"""Wraps the `__init__` method of a class to automatically set the common
|
||||||
|
attributes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exclude (Union[set, list], optional): [description]. Defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exclude = exclude or set()
|
||||||
|
exclude.add("id")
|
||||||
|
|
||||||
|
def decorator(init):
|
||||||
|
@wraps(init)
|
||||||
|
def wrapper(self, *args, **kwargs): # sourcery no-metrics
|
||||||
|
"""
|
||||||
|
Custom initializer that allows nested children initialization.
|
||||||
|
Only keys that are present as instance's class attributes are allowed.
|
||||||
|
These could be, for example, any mapped columns or relationships.
|
||||||
|
|
||||||
|
Code inspired from GitHub.
|
||||||
|
Ref: https://github.com/tiangolo/fastapi/issues/2194
|
||||||
|
"""
|
||||||
|
cls = self.__class__
|
||||||
|
model_columns = self.__mapper__.columns
|
||||||
|
relationships = self.__mapper__.relationships
|
||||||
|
|
||||||
|
for key, val in kwargs.items():
|
||||||
|
if key in exclude:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not hasattr(cls, key):
|
||||||
|
continue
|
||||||
|
# raise TypeError(f"Invalid keyword argument: {key}")
|
||||||
|
|
||||||
|
if key in model_columns:
|
||||||
|
setattr(self, key, val)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key in relationships:
|
||||||
|
relation_dir = relationships[key].direction.name
|
||||||
|
relation_cls = relationships[key].mapper.entity
|
||||||
|
use_list = relationships[key].uselist
|
||||||
|
|
||||||
|
if relation_dir == ONETOMANY.name and use_list:
|
||||||
|
instances = handle_one_to_many_list(relation_cls, val)
|
||||||
|
setattr(self, key, instances)
|
||||||
|
|
||||||
|
if relation_dir == ONETOMANY.name and not use_list:
|
||||||
|
instance = relation_cls(**val)
|
||||||
|
setattr(self, key, instance)
|
||||||
|
|
||||||
|
elif relation_dir == MANYTOONE.name and not use_list:
|
||||||
|
if isinstance(val, dict):
|
||||||
|
val = val.get("id")
|
||||||
|
|
||||||
|
if val is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected 'id' to be provided for {key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(val, (str, int)):
|
||||||
|
instance = relation_cls.get_ref(match_value=val)
|
||||||
|
setattr(self, key, instance)
|
||||||
|
|
||||||
|
elif relation_dir == MANYTOMANY.name:
|
||||||
|
if not isinstance(val, list):
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected many to many input to be of type list for {key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(val[0], dict):
|
||||||
|
val = [elem.get("id") for elem in val]
|
||||||
|
intstances = [relation_cls.get_ref(elem) for elem in val]
|
||||||
|
setattr(self, key, intstances)
|
||||||
|
|
||||||
|
return init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
@ -2,6 +2,8 @@ from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
|||||||
from requests import Session
|
from requests import Session
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, String, Table, orm
|
from sqlalchemy import Column, ForeignKey, Integer, String, Table, orm
|
||||||
|
|
||||||
|
from .._model_utils import auto_init
|
||||||
|
|
||||||
ingredients_to_units = Table(
|
ingredients_to_units = Table(
|
||||||
"ingredients_to_units",
|
"ingredients_to_units",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
@ -17,54 +19,29 @@ ingredients_to_foods = Table(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IngredientUnit(SqlAlchemyBase, BaseMixins):
|
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "ingredient_units"
|
__tablename__ = "ingredient_units"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
|
abbreviation = Column(String)
|
||||||
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_units, back_populates="unit")
|
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_units, back_populates="unit")
|
||||||
|
|
||||||
def __init__(self, name: str, description: str = None) -> None:
|
@auto_init()
|
||||||
self.name = name
|
def __init__(self, **_) -> None:
|
||||||
self.description = description
|
pass
|
||||||
|
|
||||||
@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):
|
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "ingredient_foods"
|
__tablename__ = "ingredient_foods"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_foods, back_populates="food")
|
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_foods, back_populates="food")
|
||||||
|
|
||||||
def __init__(self, name: str, description: str = None) -> None:
|
@auto_init()
|
||||||
self.name = name
|
def __init__(self, **_) -> None:
|
||||||
self.description = description
|
pass
|
||||||
|
|
||||||
@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):
|
class RecipeIngredient(SqlAlchemyBase):
|
||||||
@ -77,8 +54,8 @@ class RecipeIngredient(SqlAlchemyBase):
|
|||||||
note = Column(String) # Force Show Text - Overrides Concat
|
note = Column(String) # Force Show Text - Overrides Concat
|
||||||
|
|
||||||
# Scaling Items
|
# Scaling Items
|
||||||
unit = orm.relationship(IngredientUnit, secondary=ingredients_to_units, uselist=False)
|
unit = orm.relationship(IngredientUnitModel, secondary=ingredients_to_units, uselist=False)
|
||||||
food = orm.relationship(IngredientFood, secondary=ingredients_to_foods, uselist=False)
|
food = orm.relationship(IngredientFoodModel, secondary=ingredients_to_foods, uselist=False)
|
||||||
quantity = Column(Integer)
|
quantity = Column(Integer)
|
||||||
|
|
||||||
# Extras
|
# Extras
|
||||||
@ -86,6 +63,6 @@ class RecipeIngredient(SqlAlchemyBase):
|
|||||||
def __init__(self, title: str, note: str, unit: dict, food: dict, quantity: int, session: Session, **_) -> None:
|
def __init__(self, title: str, note: str, unit: dict, food: dict, quantity: int, session: Session, **_) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
self.note = note
|
self.note = note
|
||||||
self.unit = IngredientUnit.get_ref_or_create(session, unit)
|
self.unit = IngredientUnitModel.get_ref_or_create(session, unit)
|
||||||
self.food = IngredientFood.get_ref_or_create(session, food)
|
self.food = IngredientFoodModel.get_ref_or_create(session, food)
|
||||||
self.quantity = quantity
|
self.quantity = quantity
|
||||||
|
@ -1,33 +1,43 @@
|
|||||||
from mealie.core.root_logger import get_logger
|
from fastapi import Depends, status
|
||||||
|
from mealie.db.database import db
|
||||||
|
from mealie.db.db_setup import Session, generate_session
|
||||||
from mealie.routes.routers import UserAPIRouter
|
from mealie.routes.routers import UserAPIRouter
|
||||||
|
from mealie.schema.recipe.units_and_foods import CreateIngredientFood, IngredientFood
|
||||||
|
|
||||||
router = UserAPIRouter()
|
router = UserAPIRouter()
|
||||||
logger = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("")
|
@router.get("", response_model=list[IngredientFood])
|
||||||
async def create_food():
|
async def get_all(
|
||||||
""" Create food in the Database """
|
session: Session = Depends(generate_session),
|
||||||
# Create food
|
):
|
||||||
pass
|
""" Get unit from the Database """
|
||||||
|
# Get unit
|
||||||
|
return db.ingredient_foods.get_all(session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=IngredientFood, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_unit(unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||||
|
""" Create unit in the Database """
|
||||||
|
|
||||||
|
return db.ingredient_foods.create(session, unit)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}")
|
@router.get("/{id}")
|
||||||
async def get_food():
|
async def get_unit(id: str, session: Session = Depends(generate_session)):
|
||||||
""" Get food from the Database """
|
""" Get unit from the Database """
|
||||||
# Get food
|
|
||||||
pass
|
return db.ingredient_foods.get(session, id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{id}")
|
@router.put("/{id}")
|
||||||
async def update_food():
|
async def update_unit(id: str, unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||||
""" Update food in the Database """
|
""" Update unit in the Database """
|
||||||
# Update food
|
|
||||||
pass
|
return db.ingredient_foods.update(session, id, unit)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}")
|
@router.delete("/{id}")
|
||||||
async def delete_food():
|
async def delete_unit(id: str, session: Session = Depends(generate_session)):
|
||||||
""" Delete food from the Database """
|
""" Delete unit from the Database """
|
||||||
# Delete food
|
return db.ingredient_foods.delete(session, id)
|
||||||
pass
|
|
||||||
|
@ -1,33 +1,43 @@
|
|||||||
from mealie.core.root_logger import get_logger
|
from fastapi import Depends, status
|
||||||
|
from mealie.db.database import db
|
||||||
|
from mealie.db.db_setup import Session, generate_session
|
||||||
from mealie.routes.routers import UserAPIRouter
|
from mealie.routes.routers import UserAPIRouter
|
||||||
|
from mealie.schema.recipe.units_and_foods import CreateIngredientUnit, IngredientUnit
|
||||||
|
|
||||||
router = UserAPIRouter()
|
router = UserAPIRouter()
|
||||||
logger = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("")
|
@router.get("", response_model=list[IngredientUnit])
|
||||||
async def create_food():
|
async def get_all(
|
||||||
""" Create food in the Database """
|
session: Session = Depends(generate_session),
|
||||||
# Create food
|
):
|
||||||
pass
|
""" Get unit from the Database """
|
||||||
|
# Get unit
|
||||||
|
return db.ingredient_units.get_all(session)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=IngredientUnit, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_unit(unit: CreateIngredientUnit, session: Session = Depends(generate_session)):
|
||||||
|
""" Create unit in the Database """
|
||||||
|
|
||||||
|
return db.ingredient_units.create(session, unit)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}")
|
@router.get("/{id}")
|
||||||
async def get_food():
|
async def get_unit(id: str, session: Session = Depends(generate_session)):
|
||||||
""" Get food from the Database """
|
""" Get unit from the Database """
|
||||||
# Get food
|
|
||||||
pass
|
return db.ingredient_units.get(session, id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{id}")
|
@router.put("/{id}")
|
||||||
async def update_food():
|
async def update_unit(id: str, unit: CreateIngredientUnit, session: Session = Depends(generate_session)):
|
||||||
""" Update food in the Database """
|
""" Update unit in the Database """
|
||||||
# Update food
|
|
||||||
pass
|
return db.ingredient_units.update(session, id, unit)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}")
|
@router.delete("/{id}")
|
||||||
async def delete_food():
|
async def delete_unit(id: str, session: Session = Depends(generate_session)):
|
||||||
""" Delete food from the Database """
|
""" Delete unit from the Database """
|
||||||
# Delete food
|
return db.ingredient_units.delete(session, id)
|
||||||
pass
|
|
||||||
|
@ -11,6 +11,7 @@ from pydantic.utils import GetterDict
|
|||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
from .comments import CommentOut
|
from .comments import CommentOut
|
||||||
|
from .units_and_foods import IngredientFood, IngredientUnit
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipe(CamelModel):
|
class CreateRecipe(CamelModel):
|
||||||
@ -73,23 +74,11 @@ class Nutrition(CamelModel):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredientFood(CamelModel):
|
|
||||||
name: str = ""
|
|
||||||
description: str = ""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredientUnit(RecipeIngredientFood):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(CamelModel):
|
class RecipeIngredient(CamelModel):
|
||||||
title: Optional[str]
|
title: Optional[str]
|
||||||
note: Optional[str]
|
note: Optional[str]
|
||||||
unit: Optional[RecipeIngredientUnit]
|
unit: Optional[IngredientUnit]
|
||||||
food: Optional[RecipeIngredientFood]
|
food: Optional[IngredientFood]
|
||||||
disable_amount: bool = True
|
disable_amount: bool = True
|
||||||
quantity: int = 1
|
quantity: int = 1
|
||||||
|
|
||||||
|
24
mealie/schema/recipe/units_and_foods.py
Normal file
24
mealie/schema/recipe/units_and_foods.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
|
||||||
|
class CreateIngredientFood(CamelModel):
|
||||||
|
name: str
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class CreateIngredientUnit(CreateIngredientFood):
|
||||||
|
abbreviation: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientFood(CreateIngredientFood):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientUnit(CreateIngredientUnit):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
Loading…
x
Reference in New Issue
Block a user