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 mealie.db.models.group import *
from mealie.db.models.mealplan import *
from mealie.db.models.recipe.recipe import *
from mealie.db.models.settings import *
from mealie.db.models.shopping_list import *
from mealie.db.models.sign_up import *
from mealie.db.models.theme import *
from mealie.db.models.users import *
from .event import *
from .group import *
from .mealplan import *
from .recipe.recipe import *
from .settings import *
from .shopping_list import *
from .sign_up import *
from .theme 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 sqlalchemy import Column, ForeignKey, Integer, String, Table, orm
from .._model_utils import auto_init
ingredients_to_units = Table(
"ingredients_to_units",
SqlAlchemyBase.metadata,
@@ -17,54 +19,29 @@ ingredients_to_foods = Table(
)
class IngredientUnit(SqlAlchemyBase, BaseMixins):
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "ingredient_units"
id = Column(Integer, primary_key=True)
name = Column(String)
description = Column(String)
abbreviation = 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)
@auto_init()
def __init__(self, **_) -> None:
pass
class IngredientFood(SqlAlchemyBase, BaseMixins):
class IngredientFoodModel(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)
@auto_init()
def __init__(self, **_) -> None:
pass
class RecipeIngredient(SqlAlchemyBase):
@@ -77,8 +54,8 @@ class RecipeIngredient(SqlAlchemyBase):
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)
unit = orm.relationship(IngredientUnitModel, secondary=ingredients_to_units, uselist=False)
food = orm.relationship(IngredientFoodModel, secondary=ingredients_to_foods, uselist=False)
quantity = Column(Integer)
# Extras
@@ -86,6 +63,6 @@ class RecipeIngredient(SqlAlchemyBase):
def __init__(self, title: str, note: str, unit: dict, food: dict, quantity: int, 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.unit = IngredientUnitModel.get_ref_or_create(session, unit)
self.food = IngredientFoodModel.get_ref_or_create(session, food)
self.quantity = quantity