feat(backend): 🗃️ Add CRUD opertaions for Food and Units

This commit is contained in:
hay-kot 2021-08-22 13:10:18 -08:00
parent c894d3d880
commit 122d35ec09
9 changed files with 255 additions and 99 deletions

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View 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