mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-12 05:15:18 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user