mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-05 00:21:21 -05:00
Feature/shopping lists second try (#927)
* generate types * use generated types * ui updates * init button link for common styles * add links * setup label views * add delete confirmation * reset when not saved * link label to foods and auto set when adding to shopping list * generate types * use inheritence to manage exception handling * fix schema generation and add test for open_api generation * add header to api docs * move list consilidation to service * split list and list items controller * shopping list/list item tests - PARTIAL * enable recipe add/remove in shopping lists * generate types * linting * init global utility components * update types and add list item api * fix import cycle and database error * add container and border classes * new recipe list component * fix tests * breakout item editor * refactor item editor * update bulk actions * update input / color contrast * type generation * refactor controller dependencies * include food/unit editor * remove console.logs * fix and update type generation * fix incorrect type for column * fix postgres error * fix delete by variable * auto remove refs * fix typo
This commit is contained in:
@@ -15,9 +15,33 @@ from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
|
||||
logger = get_logger()
|
||||
settings = get_app_settings()
|
||||
|
||||
description = f"""
|
||||
Mealie is a web application for managing your recipes, meal plans, and shopping lists. This is the Restful
|
||||
API interactive documentation that can be used to explore the API. If you're justing getting started with
|
||||
the API and want to get started quickly, you can use the [API Usage | Mealie Docs](https://hay-kot.github.io/mealie/documentation/getting-started/api-usage/)
|
||||
as a reference for how to get started.
|
||||
|
||||
|
||||
As of this release <b>{APP_VERSION}</b>, Mealie is still in rapid development and therefore some of these APIs may change from version to version.
|
||||
|
||||
|
||||
If you have any questions or comments about mealie, please use the discord server to talk to the developers or other community members.
|
||||
If you'd like to file an issue, please use the [GitHub Issue Tracker | Mealie](https://github.com/hay-kot/mealie/issues/new/choose)
|
||||
|
||||
|
||||
## Helpful Links
|
||||
- [Home Page](https://mealie.io)
|
||||
- [Documentation](https://hay-kot.github.io/mealie/)
|
||||
- [Discord](https://discord.gg/QuStdQGSGK)
|
||||
- [Demo](https://demo.mealie.io)
|
||||
- [Beta](https://beta.mealie.io)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
description="A place for all your recipes",
|
||||
description=description,
|
||||
version=APP_VERSION,
|
||||
docs_url=settings.DOCS_URL,
|
||||
redoc_url=settings.REDOC_URL,
|
||||
|
||||
@@ -48,7 +48,7 @@ class GroupEventNotifierModel(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||
name = Column(String, nullable=False)
|
||||
enabled = Column(String, default=True, nullable=False)
|
||||
enabled = Column(Boolean, default=True, nullable=False)
|
||||
apprise_url = Column(String, nullable=False)
|
||||
|
||||
group = orm.relationship("Group", back_populates="group_event_notifiers", single_parent=True)
|
||||
|
||||
@@ -8,6 +8,20 @@ from .._model_utils import GUID, auto_init
|
||||
from ..recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||
|
||||
|
||||
class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_item_recipe_reference"
|
||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list_item_id = Column(GUID, ForeignKey("shopping_list_items.id"), primary_key=True)
|
||||
recipe_id = Column(Integer, ForeignKey("recipes.id"))
|
||||
recipe = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
||||
recipe_quantity = Column(Float, nullable=False)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "shopping_list_items"
|
||||
|
||||
@@ -16,7 +30,6 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
shopping_list_id = Column(GUID, ForeignKey("shopping_lists.id"))
|
||||
|
||||
# Meta
|
||||
recipe_id = Column(Integer, nullable=True)
|
||||
is_ingredient = Column(Boolean, default=True)
|
||||
position = Column(Integer, nullable=False, default=0)
|
||||
checked = Column(Boolean, default=False)
|
||||
@@ -36,8 +49,30 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
label_id = Column(GUID, ForeignKey("multi_purpose_labels.id"))
|
||||
label = orm.relationship(MultiPurposeLabel, uselist=False, back_populates="shopping_list_items")
|
||||
|
||||
# Recipe Reference
|
||||
recipe_references = orm.relationship(ShoppingListItemRecipeReference, cascade="all, delete, delete-orphan")
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "label"}
|
||||
exclude = {"id", "label", "food", "unit"}
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
__tablename__ = "shopping_list_recipe_reference"
|
||||
id = Column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
shopping_list_id = Column(GUID, ForeignKey("shopping_lists.id"), primary_key=True)
|
||||
|
||||
recipe_id = Column(Integer, ForeignKey("recipes.id"))
|
||||
recipe = orm.relationship("RecipeModel", uselist=False, back_populates="shopping_list_refs")
|
||||
|
||||
recipe_quantity = Column(Float, nullable=False)
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "recipe"}
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
@@ -59,6 +94,11 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
|
||||
recipe_references = orm.relationship(ShoppingListRecipeReference, cascade="all, delete, delete-orphan")
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "list_items"}
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
@@ -10,6 +10,7 @@ class MultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "multi_purpose_labels"
|
||||
id = Column(GUID, default=GUID.generate, primary_key=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
color = Column(String(10), nullable=False, default="")
|
||||
|
||||
group_id = Column(GUID, ForeignKey("groups.id"))
|
||||
group = orm.relationship("Group", back_populates="labels")
|
||||
|
||||
@@ -106,6 +106,18 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
date_added = sa.Column(sa.Date, default=date.today)
|
||||
date_updated = sa.Column(sa.DateTime)
|
||||
|
||||
# Shopping List Refs
|
||||
shopping_list_refs = orm.relationship(
|
||||
"ShoppingListRecipeReference",
|
||||
back_populates="recipe",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
shopping_list_item_refs = orm.relationship(
|
||||
"ShoppingListItemRecipeReference",
|
||||
back_populates="recipe",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
class Config:
|
||||
get_attr = "slug"
|
||||
exclude = {
|
||||
|
||||
@@ -9,7 +9,12 @@ from mealie.db.models.group.events import GroupEventNotifierModel
|
||||
from mealie.db.models.group.exports import GroupDataExportsModel
|
||||
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
||||
from mealie.db.models.group.preferences import GroupPreferencesModel
|
||||
from mealie.db.models.group.shopping_list import ShoppingList, ShoppingListItem
|
||||
from mealie.db.models.group.shopping_list import (
|
||||
ShoppingList,
|
||||
ShoppingListItem,
|
||||
ShoppingListItemRecipeReference,
|
||||
ShoppingListRecipeReference,
|
||||
)
|
||||
from mealie.db.models.group.webhooks import GroupWebhooksModel
|
||||
from mealie.db.models.labels import MultiPurposeLabel
|
||||
from mealie.db.models.recipe.category import Category
|
||||
@@ -28,7 +33,12 @@ from mealie.schema.events import Event as EventSchema
|
||||
from mealie.schema.group.group_events import GroupEventNotifierOut
|
||||
from mealie.schema.group.group_exports import GroupDataExport
|
||||
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
||||
from mealie.schema.group.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemRecipeRefOut,
|
||||
ShoppingListOut,
|
||||
ShoppingListRecipeRefOut,
|
||||
)
|
||||
from mealie.schema.group.invite_token import ReadInviteToken
|
||||
from mealie.schema.group.webhook import ReadWebhook
|
||||
from mealie.schema.labels import MultiPurposeLabelOut
|
||||
@@ -188,6 +198,18 @@ class AllRepositories:
|
||||
def group_shopping_list_item(self) -> RepositoryGeneric[ShoppingListItemOut, ShoppingListItem]:
|
||||
return RepositoryGeneric(self.session, pk_id, ShoppingListItem, ShoppingListItemOut)
|
||||
|
||||
@cached_property
|
||||
def group_shopping_list_item_references(
|
||||
self,
|
||||
) -> RepositoryGeneric[ShoppingListItemRecipeRefOut, ShoppingListItemRecipeReference]:
|
||||
return RepositoryGeneric(self.session, pk_id, ShoppingListItemRecipeReference, ShoppingListItemRecipeRefOut)
|
||||
|
||||
@cached_property
|
||||
def group_shopping_list_recipe_refs(
|
||||
self,
|
||||
) -> RepositoryGeneric[ShoppingListRecipeRefOut, ShoppingListRecipeReference]:
|
||||
return RepositoryGeneric(self.session, pk_id, ShoppingListRecipeReference, ShoppingListRecipeRefOut)
|
||||
|
||||
@cached_property
|
||||
def group_multi_purpose_labels(self) -> RepositoryGeneric[MultiPurposeLabelOut, MultiPurposeLabel]:
|
||||
return RepositoryGeneric(self.session, pk_id, MultiPurposeLabel, MultiPurposeLabelOut)
|
||||
|
||||
@@ -311,3 +311,16 @@ class RepositoryGeneric(Generic[T, D]):
|
||||
eff_schema.from_orm(x)
|
||||
for x in self.session.query(self.sql_model).filter(attribute_name == attr_match).all() # noqa: 711
|
||||
]
|
||||
|
||||
def create_many(self, documents: list[T]) -> list[T]:
|
||||
new_documents = []
|
||||
for document in documents:
|
||||
document = document if isinstance(document, dict) else document.dict()
|
||||
new_document = self.sql_model(session=self.session, **document)
|
||||
new_documents.append(new_document)
|
||||
|
||||
self.session.add_all(new_documents)
|
||||
self.session.commit()
|
||||
self.session.refresh(new_documents)
|
||||
|
||||
return [self.schema.from_orm(x) for x in new_documents]
|
||||
|
||||
@@ -1,59 +1,11 @@
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.db.models.group.shopping_list import ShoppingList, ShoppingListItem
|
||||
from mealie.db.models.group.shopping_list import ShoppingList
|
||||
from mealie.schema.group.group_shopping_list import ShoppingListOut, ShoppingListUpdate
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryShoppingList(RepositoryGeneric[ShoppingListOut, ShoppingList]):
|
||||
def _consolidate(self, item_list: list[ShoppingListItem]) -> ShoppingListItem:
|
||||
"""
|
||||
consolidate itterates through the shopping list provided and returns
|
||||
a consolidated list where all items that are matched against multiple values are
|
||||
de-duplicated and only the first item is kept where the quantity is updated accoridngly.
|
||||
"""
|
||||
|
||||
def can_merge(item1: ShoppingListItem, item2: ShoppingListItem) -> bool:
|
||||
"""
|
||||
can_merge checks if the two items can be merged together.
|
||||
"""
|
||||
can_merge_return = False
|
||||
|
||||
# If the items have the same food and unit they can be merged.
|
||||
if item1.unit == item2.unit and item1.food == item2.food:
|
||||
can_merge_return = True
|
||||
|
||||
# If no food or units are present check against the notes field.
|
||||
if not all([item1.food, item1.unit, item2.food, item2.unit]):
|
||||
can_merge_return = item1.note == item2.note
|
||||
|
||||
# Otherwise Assume They Can't Be Merged
|
||||
|
||||
return can_merge_return
|
||||
|
||||
consolidated_list: list[ShoppingListItem] = []
|
||||
checked_items: list[int] = []
|
||||
|
||||
for base_index, base_item in enumerate(item_list):
|
||||
if base_index in checked_items:
|
||||
continue
|
||||
|
||||
checked_items.append(base_index)
|
||||
for inner_index, inner_item in enumerate(item_list):
|
||||
if inner_index in checked_items:
|
||||
continue
|
||||
if can_merge(base_item, inner_item):
|
||||
base_item.quantity += inner_item.quantity
|
||||
checked_items.append(inner_index)
|
||||
|
||||
consolidated_list.append(base_item)
|
||||
|
||||
return consolidated_list
|
||||
|
||||
def update(self, item_id: UUID4, data: ShoppingListUpdate) -> ShoppingListOut:
|
||||
"""
|
||||
update updates the shopping list item with the provided data.
|
||||
"""
|
||||
data.list_items = self._consolidate(data.list_items)
|
||||
return super().update(item_id, data)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from abc import ABC
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.repos.all_repositories import AllRepositories
|
||||
from mealie.routes._base.checks import OperationChecks
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
@@ -27,6 +29,12 @@ class BaseUserController(ABC):
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def repos(self):
|
||||
return AllRepositories(self.deps.session)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.schema.group.group import GroupAdminUpdate
|
||||
from mealie.schema.mapper import mapper
|
||||
from mealie.schema.query import GetAll
|
||||
@@ -29,14 +27,6 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
|
||||
return self.deps.repos.groups
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base import BaseAdminController, controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
@@ -25,14 +23,6 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
|
||||
return self.deps.repos.users
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
|
||||
@@ -26,14 +26,6 @@ class RecipeCommentRoutes(BaseUserController):
|
||||
def repo(self):
|
||||
return self.deps.repos.comments
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
|
||||
@@ -25,5 +25,6 @@ router.include_router(controller_invitations.router)
|
||||
router.include_router(controller_migrations.router)
|
||||
router.include_router(controller_group_reports.router)
|
||||
router.include_router(controller_shopping_lists.router)
|
||||
router.include_router(controller_shopping_lists.item_router)
|
||||
router.include_router(controller_labels.router)
|
||||
router.include_router(controller_group_notifications.router)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.group.group_events import (
|
||||
GroupEventNotifierCreate,
|
||||
@@ -23,8 +21,7 @@ router = APIRouter(prefix="/groups/events/notifications", tags=["Group: Event No
|
||||
|
||||
|
||||
@controller(router)
|
||||
class GroupEventsNotifierController:
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
class GroupEventsNotifierController(BaseUserController):
|
||||
event_bus: EventBusService = Depends(EventBusService)
|
||||
|
||||
@cached_property
|
||||
@@ -34,14 +31,6 @@ class GroupEventsNotifierController:
|
||||
|
||||
return self.deps.repos.group_event_notifier.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.labels import (
|
||||
MultiPurposeLabelCreate,
|
||||
@@ -22,9 +20,7 @@ router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"]
|
||||
|
||||
|
||||
@controller(router)
|
||||
class MultiPurposeLabelsController:
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
class MultiPurposeLabelsController(BaseUserController):
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.deps.acting_user:
|
||||
@@ -32,14 +28,6 @@ class MultiPurposeLabelsController:
|
||||
|
||||
return self.deps.repos.group_multi_purpose_labels.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
ShoppingListCreate,
|
||||
ShoppingListItemCreate,
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemUpdate,
|
||||
ShoppingListOut,
|
||||
ShoppingListSave,
|
||||
ShoppingListSummary,
|
||||
@@ -17,10 +18,75 @@ from mealie.schema.group.group_shopping_list import (
|
||||
)
|
||||
from mealie.schema.mapper import cast
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.response.responses import SuccessResponse
|
||||
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
||||
from mealie.services.event_bus_service.message_types import EventTypes
|
||||
from mealie.services.group_services.shopping_lists import ShoppingListService
|
||||
|
||||
item_router = APIRouter(prefix="/groups/shopping/items", tags=["Group: Shopping List Items"])
|
||||
|
||||
|
||||
@controller(item_router)
|
||||
class ShoppingListItemController(BaseUserController):
|
||||
@cached_property
|
||||
def service(self):
|
||||
return ShoppingListService(self.repos)
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
return self.deps.repos.group_shopping_list_item
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[ShoppingListItemCreate, ShoppingListItemOut, ShoppingListItemCreate](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
)
|
||||
|
||||
@item_router.put("", response_model=list[ShoppingListItemOut])
|
||||
def update_many(self, data: list[ShoppingListItemUpdate]):
|
||||
# TODO: Convert to update many with single call
|
||||
|
||||
all_updates = []
|
||||
keep_ids = []
|
||||
|
||||
for item in self.service.consolidate_list_items(data):
|
||||
updated_data = self.mixins.update_one(item, item.id)
|
||||
all_updates.append(updated_data)
|
||||
keep_ids.append(updated_data.id)
|
||||
|
||||
for item in data:
|
||||
if item.id not in keep_ids:
|
||||
self.mixins.delete_one(item.id)
|
||||
|
||||
return all_updates
|
||||
|
||||
@item_router.delete("", response_model=SuccessResponse)
|
||||
def delete_many(self, ids: list[UUID4] = Query(None)):
|
||||
x = 0
|
||||
for item_id in ids:
|
||||
self.mixins.delete_one(item_id)
|
||||
x += 1
|
||||
|
||||
return SuccessResponse.respond(message=f"Successfully deleted {x} items")
|
||||
|
||||
@item_router.post("", response_model=ShoppingListItemOut, status_code=201)
|
||||
def create_one(self, data: ShoppingListItemCreate):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@item_router.get("/{item_id}", response_model=ShoppingListItemOut)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@item_router.put("/{item_id}", response_model=ShoppingListItemOut)
|
||||
def update_one(self, item_id: UUID4, data: ShoppingListItemUpdate):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@item_router.delete("/{item_id}", response_model=ShoppingListItemOut)
|
||||
def delete_one(self, item_id: UUID4):
|
||||
return self.mixins.delete_one(item_id) # type: ignore
|
||||
|
||||
|
||||
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"])
|
||||
|
||||
|
||||
@@ -34,23 +100,12 @@ class ShoppingListController(BaseUserController):
|
||||
|
||||
@cached_property
|
||||
def repo(self):
|
||||
if not self.deps.acting_user:
|
||||
raise Exception("No user is logged in.")
|
||||
|
||||
return self.deps.repos.group_shopping_lists.by_group(self.deps.acting_user.group_id)
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
# =======================================================================
|
||||
# CRUD Operations
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def mixins(self) -> CrudMixins:
|
||||
return CrudMixins(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
||||
|
||||
@@ -58,7 +113,7 @@ class ShoppingListController(BaseUserController):
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit, override_schema=ShoppingListSummary)
|
||||
|
||||
@router.post("", response_model=ShoppingListOut)
|
||||
@router.post("", response_model=ShoppingListOut, status_code=201)
|
||||
def create_one(self, data: ShoppingListCreate):
|
||||
save_data = cast(data, ShoppingListSave, group_id=self.deps.acting_user.group_id)
|
||||
val = self.mixins.create_one(save_data)
|
||||
@@ -74,7 +129,7 @@ class ShoppingListController(BaseUserController):
|
||||
|
||||
@router.get("/{item_id}", response_model=ShoppingListOut)
|
||||
def get_one(self, item_id: UUID4):
|
||||
return self.repo.get_one(item_id)
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=ShoppingListOut)
|
||||
def update_one(self, item_id: UUID4, data: ShoppingListUpdate):
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood
|
||||
|
||||
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"])
|
||||
|
||||
@@ -19,38 +17,30 @@ class IngredientFoodsController(BaseUserController):
|
||||
def repo(self):
|
||||
return self.deps.repos.ingredient_foods
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreateIngredientUnit, IngredientUnit, CreateIngredientUnit](
|
||||
return CrudMixins[CreateIngredientFood, IngredientFood, CreateIngredientFood](
|
||||
self.repo,
|
||||
self.deps.logger,
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.get("", response_model=list[IngredientUnit])
|
||||
@router.get("", response_model=list[IngredientFood])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit)
|
||||
|
||||
@router.post("", response_model=IngredientUnit, status_code=201)
|
||||
def create_one(self, data: CreateIngredientUnit):
|
||||
@router.post("", response_model=IngredientFood, status_code=201)
|
||||
def create_one(self, data: CreateIngredientFood):
|
||||
return self.mixins.create_one(data)
|
||||
|
||||
@router.get("/{item_id}", response_model=IngredientUnit)
|
||||
@router.get("/{item_id}", response_model=IngredientFood)
|
||||
def get_one(self, item_id: int):
|
||||
return self.mixins.get_one(item_id)
|
||||
|
||||
@router.put("/{item_id}", response_model=IngredientUnit)
|
||||
def update_one(self, item_id: int, data: CreateIngredientUnit):
|
||||
@router.put("/{item_id}", response_model=IngredientFood)
|
||||
def update_one(self, item_id: int, data: CreateIngredientFood):
|
||||
return self.mixins.update_one(data, item_id)
|
||||
|
||||
@router.delete("/{item_id}", response_model=IngredientUnit)
|
||||
@router.delete("/{item_id}", response_model=IngredientFood)
|
||||
def delete_one(self, item_id: int):
|
||||
return self.mixins.delete_one(item_id)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from functools import cached_property
|
||||
from typing import Type
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.core.exceptions import mealie_registered_exceptions
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
@@ -19,14 +17,6 @@ class IngredientUnitsController(BaseUserController):
|
||||
def repo(self):
|
||||
return self.deps.repos.ingredient_units
|
||||
|
||||
def registered_exceptions(self, ex: Type[Exception]) -> str:
|
||||
|
||||
registered = {
|
||||
**mealie_registered_exceptions(self.deps.t),
|
||||
}
|
||||
|
||||
return registered.get(ex, "An unexpected error occurred.")
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return CrudMixins[CreateIngredientUnit, IngredientUnit, CreateIngredientUnit](
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
@@ -6,6 +8,19 @@ from pydantic import UUID4
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
||||
|
||||
|
||||
class ShoppingListItemRecipeRef(CamelModel):
|
||||
recipe_id: int
|
||||
recipe_quantity: float
|
||||
|
||||
|
||||
class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRef):
|
||||
id: UUID4
|
||||
shopping_list_item_id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class ShoppingListItemCreate(CamelModel):
|
||||
shopping_list_id: UUID4
|
||||
checked: bool = False
|
||||
@@ -16,30 +31,41 @@ class ShoppingListItemCreate(CamelModel):
|
||||
note: Optional[str] = ""
|
||||
quantity: float = 1
|
||||
unit_id: int = None
|
||||
unit: IngredientUnit = None
|
||||
unit: Optional[IngredientUnit]
|
||||
food_id: int = None
|
||||
food: IngredientFood = None
|
||||
recipe_id: Optional[int] = None
|
||||
food: Optional[IngredientFood]
|
||||
|
||||
label_id: Optional[UUID4] = None
|
||||
recipe_references: list[ShoppingListItemRecipeRef] = []
|
||||
|
||||
|
||||
class ShoppingListItemOut(ShoppingListItemCreate):
|
||||
class ShoppingListItemUpdate(ShoppingListItemCreate):
|
||||
id: UUID4
|
||||
label: "Optional[MultiPurposeLabelSummary]" = None
|
||||
|
||||
|
||||
class ShoppingListItemOut(ShoppingListItemUpdate):
|
||||
label: Optional[MultiPurposeLabelSummary]
|
||||
recipe_references: list[ShoppingListItemRecipeRefOut] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class ShoppingListCreate(CamelModel):
|
||||
"""
|
||||
Create Shopping List
|
||||
"""
|
||||
|
||||
name: str = None
|
||||
|
||||
|
||||
class ShoppingListRecipeRefOut(CamelModel):
|
||||
id: UUID4
|
||||
shopping_list_id: UUID4
|
||||
recipe_id: int
|
||||
recipe_quantity: float
|
||||
recipe: RecipeSummary
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class ShoppingListSave(ShoppingListCreate):
|
||||
group_id: UUID4
|
||||
|
||||
@@ -56,10 +82,14 @@ class ShoppingListUpdate(ShoppingListSummary):
|
||||
|
||||
|
||||
class ShoppingListOut(ShoppingListUpdate):
|
||||
recipe_references: list[ShoppingListRecipeRefOut]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
from mealie.schema.labels import MultiPurposeLabelSummary
|
||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
|
||||
from mealie.schema.recipe.recipe import RecipeSummary
|
||||
|
||||
ShoppingListRecipeRefOut.update_forward_refs()
|
||||
ShoppingListItemOut.update_forward_refs()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.schema.recipe import IngredientFood
|
||||
|
||||
|
||||
class MultiPurposeLabelCreate(CamelModel):
|
||||
name: str
|
||||
color: str = ""
|
||||
|
||||
|
||||
class MultiPurposeLabelSave(MultiPurposeLabelCreate):
|
||||
@@ -24,13 +25,14 @@ class MultiPurposeLabelSummary(MultiPurposeLabelUpdate):
|
||||
|
||||
|
||||
class MultiPurposeLabelOut(MultiPurposeLabelUpdate):
|
||||
shopping_list_items: "list[ShoppingListItemOut]" = []
|
||||
foods: list[IngredientFood] = []
|
||||
# shopping_list_items: list[ShoppingListItemOut] = []
|
||||
# foods: list[IngredientFood] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
from mealie.schema.group.group_shopping_list import ShoppingListItemOut
|
||||
# from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||
# from mealie.schema.group.group_shopping_list import ShoppingListItemOut
|
||||
|
||||
MultiPurposeLabelOut.update_forward_refs()
|
||||
# MultiPurposeLabelOut.update_forward_refs()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
@@ -13,7 +15,6 @@ from mealie.db.models.recipe.recipe import RecipeModel
|
||||
|
||||
from .recipe_asset import RecipeAsset
|
||||
from .recipe_comments import RecipeCommentOut
|
||||
from .recipe_ingredient import RecipeIngredient
|
||||
from .recipe_notes import RecipeNote
|
||||
from .recipe_nutrition import Nutrition
|
||||
from .recipe_settings import RecipeSettings
|
||||
@@ -91,25 +92,25 @@ class RecipeSummary(CamelModel):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("tags", always=True, pre=True)
|
||||
@validator("tags", always=True, pre=True, allow_reuse=True)
|
||||
def validate_tags(cats: list[Any]): # type: ignore
|
||||
if isinstance(cats, list) and cats and isinstance(cats[0], str):
|
||||
return [RecipeTag(name=c, slug=slugify(c)) for c in cats]
|
||||
return cats
|
||||
|
||||
@validator("recipe_category", always=True, pre=True)
|
||||
@validator("recipe_category", always=True, pre=True, allow_reuse=True)
|
||||
def validate_categories(cats: list[Any]): # type: ignore
|
||||
if isinstance(cats, list) and cats and isinstance(cats[0], str):
|
||||
return [RecipeCategory(name=c, slug=slugify(c)) for c in cats]
|
||||
return cats
|
||||
|
||||
@validator("group_id", always=True, pre=True)
|
||||
@validator("group_id", always=True, pre=True, allow_reuse=True)
|
||||
def validate_group_id(group_id: Any):
|
||||
if isinstance(group_id, int):
|
||||
return uuid4()
|
||||
return group_id
|
||||
|
||||
@validator("user_id", always=True, pre=True)
|
||||
@validator("user_id", always=True, pre=True, allow_reuse=True)
|
||||
def validate_user_id(user_id: Any):
|
||||
if isinstance(user_id, int):
|
||||
return uuid4()
|
||||
@@ -164,14 +165,14 @@ class Recipe(RecipeSummary):
|
||||
"extras": {x.key_name: x.value for x in name_orm.extras},
|
||||
}
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
@validator("slug", always=True, pre=True, allow_reuse=True)
|
||||
def validate_slug(slug: str, values):
|
||||
if not values.get("name"):
|
||||
return slug
|
||||
|
||||
return slugify(values["name"])
|
||||
|
||||
@validator("recipe_ingredient", always=True, pre=True)
|
||||
@validator("recipe_ingredient", always=True, pre=True, allow_reuse=True)
|
||||
def validate_ingredients(recipe_ingredient, values):
|
||||
if not recipe_ingredient or not isinstance(recipe_ingredient, list):
|
||||
return recipe_ingredient
|
||||
@@ -180,3 +181,9 @@ class Recipe(RecipeSummary):
|
||||
return [RecipeIngredient(note=x) for x in recipe_ingredient]
|
||||
|
||||
return recipe_ingredient
|
||||
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient
|
||||
|
||||
RecipeSummary.update_forward_refs()
|
||||
Recipe.update_forward_refs()
|
||||
|
||||
@@ -9,23 +9,23 @@ class ExportTypes(str, enum.Enum):
|
||||
JSON = "json"
|
||||
|
||||
|
||||
class _ExportBase(CamelModel):
|
||||
class ExportBase(CamelModel):
|
||||
recipes: list[str]
|
||||
|
||||
|
||||
class ExportRecipes(_ExportBase):
|
||||
class ExportRecipes(ExportBase):
|
||||
export_type: ExportTypes = ExportTypes.JSON
|
||||
|
||||
|
||||
class AssignCategories(_ExportBase):
|
||||
class AssignCategories(ExportBase):
|
||||
categories: list[CategoryBase]
|
||||
|
||||
|
||||
class AssignTags(_ExportBase):
|
||||
class AssignTags(ExportBase):
|
||||
tags: list[TagBase]
|
||||
|
||||
|
||||
class DeleteRecipes(_ExportBase):
|
||||
class DeleteRecipes(ExportBase):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from typing import Optional, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
from pydantic import Field
|
||||
|
||||
uuid4()
|
||||
from pydantic import UUID4, Field
|
||||
|
||||
|
||||
class CreateIngredientFood(CamelModel):
|
||||
class UnitFoodBase(CamelModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
class CreateIngredientUnit(CreateIngredientFood):
|
||||
fraction: bool = True
|
||||
abbreviation: str = ""
|
||||
class CreateIngredientFood(UnitFoodBase):
|
||||
label_id: UUID4 = None
|
||||
label: MultiPurposeLabelSummary = None
|
||||
|
||||
|
||||
class IngredientFood(CreateIngredientFood):
|
||||
@@ -25,6 +25,11 @@ class IngredientFood(CreateIngredientFood):
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class CreateIngredientUnit(UnitFoodBase):
|
||||
fraction: bool = True
|
||||
abbreviation: str = ""
|
||||
|
||||
|
||||
class IngredientUnit(CreateIngredientUnit):
|
||||
id: int
|
||||
|
||||
@@ -77,3 +82,9 @@ class IngredientsRequest(CamelModel):
|
||||
class IngredientRequest(CamelModel):
|
||||
parser: RegisteredParser = RegisteredParser.nlp
|
||||
ingredient: str
|
||||
|
||||
|
||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
|
||||
|
||||
CreateIngredientFood.update_forward_refs()
|
||||
IngredientFood.update_forward_refs()
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
from .recipe_keys import *
|
||||
|
||||
@@ -1,19 +1,96 @@
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.group import ShoppingListOut
|
||||
from mealie.schema.group.group_shopping_list import ShoppingListItemCreate
|
||||
from mealie.schema.group import ShoppingListItemCreate, ShoppingListOut
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemRecipeRef,
|
||||
ShoppingListItemUpdate,
|
||||
)
|
||||
|
||||
|
||||
class ShoppingListService:
|
||||
def __init__(self, repos: AllRepositories):
|
||||
self.repos = repos
|
||||
self.repo = repos.group_shopping_lists
|
||||
self.shopping_lists = repos.group_shopping_lists
|
||||
self.list_items = repos.group_shopping_list_item
|
||||
self.list_item_refs = repos.group_shopping_list_item_references
|
||||
self.list_refs = repos.group_shopping_list_recipe_refs
|
||||
|
||||
@staticmethod
|
||||
def can_merge(item1: ShoppingListItemOut, item2: ShoppingListItemOut) -> bool:
|
||||
"""
|
||||
can_merge checks if the two items can be merged together.
|
||||
"""
|
||||
|
||||
# If no food or units are present check against the notes field.
|
||||
if not all([item1.food, item1.unit, item2.food, item2.unit]):
|
||||
return item1.note == item2.note
|
||||
|
||||
# If the items have the same food and unit they can be merged.
|
||||
if item1.unit == item2.unit and item1.food == item2.food:
|
||||
return True
|
||||
|
||||
# Otherwise Assume They Can't Be Merged
|
||||
return False
|
||||
|
||||
def consolidate_list_items(self, item_list: list[ShoppingListItemOut]) -> list[ShoppingListItemOut]:
|
||||
"""
|
||||
itterates through the shopping list provided and returns
|
||||
a consolidated list where all items that are matched against multiple values are
|
||||
de-duplicated and only the first item is kept where the quantity is updated accoridngly.
|
||||
"""
|
||||
|
||||
consolidated_list: list[ShoppingListItemOut] = []
|
||||
checked_items: list[int] = []
|
||||
|
||||
for base_index, base_item in enumerate(item_list):
|
||||
if base_index in checked_items:
|
||||
continue
|
||||
|
||||
checked_items.append(base_index)
|
||||
for inner_index, inner_item in enumerate(item_list):
|
||||
if inner_index in checked_items:
|
||||
continue
|
||||
if ShoppingListService.can_merge(base_item, inner_item):
|
||||
# Set Quantity
|
||||
base_item.quantity += inner_item.quantity
|
||||
|
||||
# Set References
|
||||
new_refs = []
|
||||
for ref in inner_item.recipe_references:
|
||||
ref.shopping_list_item_id = base_item.id
|
||||
new_refs.append(ref)
|
||||
|
||||
base_item.recipe_references.extend(new_refs)
|
||||
checked_items.append(inner_index)
|
||||
|
||||
consolidated_list.append(base_item)
|
||||
|
||||
return consolidated_list
|
||||
|
||||
def consolidate_and_save(self, data: list[ShoppingListItemUpdate]):
|
||||
# TODO: Convert to update many with single call
|
||||
|
||||
all_updates = []
|
||||
keep_ids = []
|
||||
|
||||
for item in self.consolidate_list_items(data):
|
||||
updated_data = self.list_items.update(item.id, item)
|
||||
all_updates.append(updated_data)
|
||||
keep_ids.append(updated_data.id)
|
||||
|
||||
for item in data:
|
||||
if item.id not in keep_ids:
|
||||
self.list_items.delete(item.id)
|
||||
|
||||
return all_updates
|
||||
|
||||
# =======================================================================
|
||||
# Methods
|
||||
|
||||
def add_recipe_ingredients_to_list(self, list_id: UUID4, recipe_id: int) -> ShoppingListOut:
|
||||
recipe = self.repos.recipes.get_one(recipe_id, "id")
|
||||
shopping_list = self.repo.get_one(list_id)
|
||||
|
||||
to_create = []
|
||||
|
||||
for ingredient in recipe.recipe_ingredient:
|
||||
@@ -23,6 +100,12 @@ class ShoppingListService:
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
label_id = None
|
||||
try:
|
||||
label_id = ingredient.food.label.id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
unit_id = None
|
||||
try:
|
||||
unit_id = ingredient.unit.id
|
||||
@@ -32,19 +115,77 @@ class ShoppingListService:
|
||||
to_create.append(
|
||||
ShoppingListItemCreate(
|
||||
shopping_list_id=list_id,
|
||||
is_food=True,
|
||||
is_food=not recipe.settings.disable_amount,
|
||||
food_id=food_id,
|
||||
unit_id=unit_id,
|
||||
quantity=ingredient.quantity,
|
||||
note=ingredient.note,
|
||||
label_id=label_id,
|
||||
recipe_id=recipe_id,
|
||||
recipe_references=[
|
||||
ShoppingListItemRecipeRef(
|
||||
recipe_id=recipe_id,
|
||||
recipe_quantity=ingredient.quantity,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
shopping_list.list_items.extend(to_create)
|
||||
return self.repo.update(shopping_list.id, shopping_list)
|
||||
for item in to_create:
|
||||
self.repos.group_shopping_list_item.create(item)
|
||||
|
||||
updated_list = self.shopping_lists.get_one(list_id)
|
||||
updated_list.list_items = self.consolidate_and_save(updated_list.list_items)
|
||||
|
||||
not_found = True
|
||||
for refs in updated_list.recipe_references:
|
||||
if refs.recipe_id == recipe_id:
|
||||
refs.recipe_quantity += 1
|
||||
not_found = False
|
||||
|
||||
if not_found:
|
||||
updated_list.recipe_references.append(ShoppingListItemRecipeRef(recipe_id=recipe_id, recipe_quantity=1))
|
||||
|
||||
updated_list = self.shopping_lists.update(updated_list.id, updated_list)
|
||||
|
||||
return updated_list
|
||||
|
||||
def remove_recipe_ingredients_from_list(self, list_id: UUID4, recipe_id: int) -> ShoppingListOut:
|
||||
shopping_list = self.repo.get_one(list_id)
|
||||
shopping_list.list_items = [x for x in shopping_list.list_items if x.recipe_id != recipe_id]
|
||||
return self.repo.update(shopping_list.id, shopping_list)
|
||||
shopping_list = self.shopping_lists.get_one(list_id)
|
||||
|
||||
for item in shopping_list.list_items:
|
||||
found = False
|
||||
|
||||
for ref in item.recipe_references:
|
||||
remove_qty = 0
|
||||
|
||||
if ref.recipe_id == recipe_id:
|
||||
self.list_item_refs.delete(ref.id)
|
||||
item.recipe_references.remove(ref)
|
||||
found = True
|
||||
remove_qty = ref.recipe_quantity
|
||||
break # only remove one instance of the recipe for each item
|
||||
|
||||
# If the item was found decrement the quantity by the remove_qty
|
||||
if found:
|
||||
item.quantity = item.quantity - remove_qty
|
||||
|
||||
if item.quantity <= 0:
|
||||
self.list_items.delete(item.id)
|
||||
else:
|
||||
self.list_items.update(item.id, item)
|
||||
|
||||
# Decrament the list recipe reference count
|
||||
for ref in shopping_list.recipe_references:
|
||||
if ref.recipe_id == recipe_id:
|
||||
ref.recipe_quantity -= 1
|
||||
|
||||
if ref.recipe_quantity <= 0:
|
||||
self.list_refs.delete(ref.id)
|
||||
|
||||
else:
|
||||
self.list_refs.update(ref.id, ref)
|
||||
break
|
||||
|
||||
# Save Changes
|
||||
return self.shopping_lists.get(shopping_list.id)
|
||||
|
||||
Reference in New Issue
Block a user