feat: add support for API extras on shopping lists, shopping list items, and food data (#1619)

* added api extras to other tables
genericized api extras model from recipes
added extras column to ingredient foods
added extras column to shopping lists
added extras column to shopping list items

* updated alembic version test

* made mypy happy

* added TODO on test that does nothing

* added extras tests for lists, items, and foods

* added docs for new extras

* modified alembic versions to eliminate branching
This commit is contained in:
Michael Genson
2022-09-27 21:53:22 -05:00
committed by GitHub
parent db70a210a2
commit 8271c3001e
12 changed files with 300 additions and 33 deletions

View File

@@ -2,6 +2,7 @@ from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String, orm
from sqlalchemy.ext.orderinglist import ordering_list
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models.recipe.api_extras import ShoppingListExtras, ShoppingListItemExtras, api_extras
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
@@ -38,6 +39,7 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
note = Column(String)
is_food = Column(Boolean, default=False)
extras: list[ShoppingListItemExtras] = orm.relationship("ShoppingListItemExtras", cascade="all, delete-orphan")
# Scaling Items
unit_id = Column(GUID, ForeignKey("ingredient_units.id"))
@@ -55,6 +57,7 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
class Config:
exclude = {"id", "label", "food", "unit"}
@api_extras
@auto_init()
def __init__(self, **_) -> None:
pass
@@ -95,10 +98,12 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
)
recipe_references = orm.relationship(ShoppingListRecipeReference, cascade="all, delete, delete-orphan")
extras: list[ShoppingListExtras] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
class Config:
exclude = {"id", "list_items"}
@api_extras
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -4,13 +4,54 @@ from mealie.db.models._model_base import SqlAlchemyBase
from mealie.db.models._model_utils.guid import GUID
class ApiExtras(SqlAlchemyBase):
__tablename__ = "api_extras"
def api_extras(func):
"""Decorator function to unpack the extras into a dict; requires an "extras" column"""
def wrapper(*args, **kwargs):
extras = kwargs.pop("extras")
if extras is None:
extras = []
else:
extras = [{"key": key, "value": value} for key, value in extras.items()]
return func(*args, extras=extras, **kwargs)
return wrapper
class ExtrasGeneric:
"""
Template for API extensions
This class is not an actual table, so it does not inherit from SqlAlchemyBase
"""
id = sa.Column(sa.Integer, primary_key=True)
recipee_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
key_name = sa.Column(sa.String)
value = sa.Column(sa.String)
def __init__(self, key, value) -> None:
self.key_name = key
self.value = value
# used specifically for recipe extras
class ApiExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "api_extras"
recipee_id = sa.Column(GUID, sa.ForeignKey("recipes.id"))
class IngredientFoodExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "ingredient_food_extras"
ingredient_food_id = sa.Column(GUID, sa.ForeignKey("ingredient_foods.id"))
class ShoppingListExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "shopping_list_extras"
shopping_list_id = sa.Column(GUID, sa.ForeignKey("shopping_lists.id"))
class ShoppingListItemExtras(ExtrasGeneric, SqlAlchemyBase):
__tablename__ = "shopping_list_item_extras"
shopping_list_item_id = sa.Column(GUID, sa.ForeignKey("shopping_list_items.id"))

View File

@@ -2,6 +2,7 @@ from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String, orm
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.labels import MultiPurposeLabel
from mealie.db.models.recipe.api_extras import IngredientFoodExtras, api_extras
from .._model_utils import auto_init
from .._model_utils.guid import GUID
@@ -38,10 +39,12 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
name = Column(String)
description = Column(String)
ingredients = orm.relationship("RecipeIngredient", back_populates="food")
extras: list[IngredientFoodExtras] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan")
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
label = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="foods")
@api_extras
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@@ -10,7 +10,7 @@ from mealie.db.models._model_utils.guid import GUID
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from ..users.user_to_favorite import users_to_favorites
from .api_extras import ApiExtras
from .api_extras import ApiExtras, api_extras
from .assets import RecipeAsset
from .category import recipes_to_categories
from .comment import RecipeComment
@@ -24,21 +24,6 @@ from .tag import recipes_to_tags
from .tool import recipes_to_tools
# Decorator function to unpack the extras into a dict
def recipe_extras(func):
def wrapper(*args, **kwargs):
extras = kwargs.pop("extras")
if extras is None:
extras = []
else:
extras = [{"key": key, "value": value} for key, value in extras.items()]
return func(*args, extras=extras, **kwargs)
return wrapper
class RecipeModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "recipes"
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="recipe_slug_group_id_key"),)
@@ -139,7 +124,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
assert name != ""
return name
@recipe_extras
@api_extras
@auto_init()
def __init__(
self,

View File

@@ -4,7 +4,9 @@ from datetime import datetime
from typing import Optional, Union
from pydantic import UUID4
from pydantic.utils import GetterDict
from mealie.db.models.group.shopping_list import ShoppingList, ShoppingListItem
from mealie.schema._mealie import MealieModel
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
from mealie.schema.response.pagination import PaginationBase
@@ -39,6 +41,7 @@ class ShoppingListItemCreate(MealieModel):
label_id: Optional[UUID4] = None
recipe_references: list[ShoppingListItemRecipeRef] = []
extras: Optional[dict] = {}
created_at: Optional[datetime]
update_at: Optional[datetime]
@@ -55,9 +58,17 @@ class ShoppingListItemOut(ShoppingListItemUpdate):
class Config:
orm_mode = True
@classmethod
def getter_dict(cls, name_orm: ShoppingListItem):
return {
**GetterDict(name_orm),
"extras": {x.key_name: x.value for x in name_orm.extras},
}
class ShoppingListCreate(MealieModel):
name: str = None
extras: Optional[dict] = {}
created_at: Optional[datetime]
update_at: Optional[datetime]
@@ -84,6 +95,13 @@ class ShoppingListSummary(ShoppingListSave):
class Config:
orm_mode = True
@classmethod
def getter_dict(cls, name_orm: ShoppingList):
return {
**GetterDict(name_orm),
"extras": {x.key_name: x.value for x in name_orm.extras},
}
class ShoppingListPagination(PaginationBase):
items: list[ShoppingListSummary]

View File

@@ -6,7 +6,9 @@ from typing import Optional, Union
from uuid import UUID, uuid4
from pydantic import UUID4, Field, validator
from pydantic.utils import GetterDict
from mealie.db.models.recipe.ingredient import IngredientFoodModel
from mealie.schema._mealie import MealieModel
from mealie.schema._mealie.types import NoneFloat
from mealie.schema.response.pagination import PaginationBase
@@ -15,6 +17,7 @@ from mealie.schema.response.pagination import PaginationBase
class UnitFoodBase(MealieModel):
name: str
description: str = ""
extras: Optional[dict] = {}
class CreateIngredientFood(UnitFoodBase):
@@ -34,6 +37,13 @@ class IngredientFood(CreateIngredientFood):
class Config:
orm_mode = True
@classmethod
def getter_dict(cls, name_orm: IngredientFoodModel):
return {
**GetterDict(name_orm),
"extras": {x.key_name: x.value for x in name_orm.extras},
}
class IngredientFoodPagination(PaginationBase):
items: list[IngredientFood]