feat: Improved Recipe Scaling Support and Shopping List Recipe Refactor (#1847)
* propogate scale changes to print view * fixed incorrect variable reference * refactored shopping list recipe routes cleaned up existing logic added support for recipe scaling * updated current revision * adding to shopping list respects UI recipe scale * added field annotations * added tests for recipe scaling * made column nullable and set to 1 during migration
This commit is contained in:
parent
d9c39cc1d0
commit
46cc3898ab
13 changed files with 344 additions and 49 deletions
|
@ -0,0 +1,29 @@
|
|||
"""add recipe_scale to shopping list item ref
|
||||
|
||||
Revision ID: 167eb69066ad
|
||||
Revises: 1923519381ad
|
||||
Create Date: 2022-11-22 03:42:45.494567
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "167eb69066ad"
|
||||
down_revision = "1923519381ad"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("shopping_list_item_recipe_reference", sa.Column("recipe_scale", sa.Float(), nullable=True))
|
||||
op.execute("UPDATE shopping_list_item_recipe_reference SET recipe_scale = 1")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("shopping_list_item_recipe_reference", "recipe_scale")
|
||||
# ### end Alembic commands ###
|
|
@ -51,6 +51,7 @@
|
|||
color="info"
|
||||
:card-menu="false"
|
||||
:recipe-id="recipe.id"
|
||||
:recipe-scale="recipeScale"
|
||||
:use-items="{
|
||||
delete: false,
|
||||
edit: false,
|
||||
|
@ -105,6 +106,10 @@ export default defineComponent({
|
|||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
open: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
|
|
|
@ -199,6 +199,10 @@ export default defineComponent({
|
|||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
/**
|
||||
* Optional group ID prop that is only _required_ when the
|
||||
* public URL is requested. If the public URL button is pressed
|
||||
|
@ -316,7 +320,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
async function addRecipeToList(listId: string) {
|
||||
const { data } = await api.shopping.lists.addRecipe(listId, props.recipeId);
|
||||
const { data } = await api.shopping.lists.addRecipe(listId, props.recipeId, props.recipeScale);
|
||||
if (data) {
|
||||
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
|
||||
state.shoppingListDialog = false;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-container :class="{ 'pa-0': $vuetify.breakpoint.smAndDown }">
|
||||
<v-card :flat="$vuetify.breakpoint.smAndDown" class="d-print-none">
|
||||
<RecipePageHeader :recipe="recipe" :landscape="landscape" @save="saveRecipe" @delete="deleteRecipe" />
|
||||
<RecipePageHeader :recipe="recipe" :recipe-scale="scale" :landscape="landscape" @save="saveRecipe" @delete="deleteRecipe" />
|
||||
<LazyRecipeJsonEditor v-if="isEditJSON" v-model="recipe" class="mt-10" :options="EDITOR_OPTIONS" />
|
||||
<v-card-text v-else>
|
||||
<!--
|
||||
|
@ -70,7 +70,7 @@
|
|||
:recipe="recipe"
|
||||
class="px-1 my-4 d-print-none"
|
||||
/>
|
||||
<RecipePrintView :recipe="recipe" />
|
||||
<RecipePrintView :recipe="recipe" :scale="scale" />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
v-if="user.id"
|
||||
:recipe="recipe"
|
||||
:slug="recipe.slug"
|
||||
:recipe-scale="recipeScale"
|
||||
:locked="user.id !== recipe.userId && recipe.settings.locked"
|
||||
:name="recipe.name"
|
||||
:logged-in="$auth.loggedIn"
|
||||
|
@ -85,6 +86,10 @@ export default defineComponent({
|
|||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
landscape: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
|
@ -96,6 +96,10 @@ export default defineComponent({
|
|||
type: Object as () => Recipe,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// Group ingredients by section so we can style them independently
|
||||
|
@ -181,7 +185,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
function parseText(ingredient: RecipeIngredient) {
|
||||
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false);
|
||||
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -24,8 +24,8 @@ export class ShoppingListsApi extends BaseCRUDAPI<ShoppingListCreate, ShoppingLi
|
|||
baseRoute = routes.shoppingLists;
|
||||
itemRoute = routes.shoppingListsId;
|
||||
|
||||
async addRecipe(itemId: string, recipeId: string) {
|
||||
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {});
|
||||
async addRecipe(itemId: string, recipeId: string, recipeIncrementQuantity = 1) {
|
||||
return await this.requests.post(routes.shoppingListIdAddRecipe(itemId, recipeId), {recipeIncrementQuantity});
|
||||
}
|
||||
|
||||
async removeRecipe(itemId: string, recipeId: string) {
|
||||
|
|
|
@ -17,6 +17,7 @@ class ShoppingListItemRecipeReference(BaseMixins, SqlAlchemyBase):
|
|||
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True)
|
||||
recipe = orm.relationship("RecipeModel", back_populates="shopping_list_item_refs")
|
||||
recipe_quantity = Column(Float, nullable=False)
|
||||
recipe_scale = Column(Float, nullable=False, default=1)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
|
|
@ -7,12 +7,14 @@ from mealie.routes._base.base_controllers import BaseCrudController
|
|||
from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.schema.group.group_shopping_list import (
|
||||
ShoppingListAddRecipeParams,
|
||||
ShoppingListCreate,
|
||||
ShoppingListItemCreate,
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemUpdate,
|
||||
ShoppingListOut,
|
||||
ShoppingListPagination,
|
||||
ShoppingListRemoveRecipeParams,
|
||||
ShoppingListSave,
|
||||
ShoppingListSummary,
|
||||
ShoppingListUpdate,
|
||||
|
@ -218,13 +220,17 @@ class ShoppingListController(BaseCrudController):
|
|||
# Other Operations
|
||||
|
||||
@router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
||||
def add_recipe_ingredients_to_list(self, item_id: UUID4, recipe_id: UUID4):
|
||||
def add_recipe_ingredients_to_list(
|
||||
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListAddRecipeParams | None = None
|
||||
):
|
||||
(
|
||||
shopping_list,
|
||||
new_shopping_list_items,
|
||||
updated_shopping_list_items,
|
||||
deleted_shopping_list_items,
|
||||
) = self.service.add_recipe_ingredients_to_list(item_id, recipe_id)
|
||||
) = self.service.add_recipe_ingredients_to_list(
|
||||
item_id, recipe_id, data.recipe_increment_quantity if data else 1
|
||||
)
|
||||
|
||||
if new_shopping_list_items:
|
||||
self.publish_event(
|
||||
|
@ -263,12 +269,16 @@ class ShoppingListController(BaseCrudController):
|
|||
return shopping_list
|
||||
|
||||
@router.delete("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut)
|
||||
def remove_recipe_ingredients_from_list(self, item_id: UUID4, recipe_id: UUID4):
|
||||
def remove_recipe_ingredients_from_list(
|
||||
self, item_id: UUID4, recipe_id: UUID4, data: ShoppingListRemoveRecipeParams | None = None
|
||||
):
|
||||
(
|
||||
shopping_list,
|
||||
updated_shopping_list_items,
|
||||
deleted_shopping_list_items,
|
||||
) = self.service.remove_recipe_ingredients_from_list(item_id, recipe_id)
|
||||
) = self.service.remove_recipe_ingredients_from_list(
|
||||
item_id, recipe_id, data.recipe_decrement_quantity if data else 1
|
||||
)
|
||||
|
||||
if updated_shopping_list_items:
|
||||
self.publish_event(
|
||||
|
|
|
@ -15,6 +15,10 @@ from mealie.schema.response.pagination import PaginationBase
|
|||
class ShoppingListItemRecipeRef(MealieModel):
|
||||
recipe_id: UUID4
|
||||
recipe_quantity: NoneFloat = 0
|
||||
"""the quantity of this item in a single recipe (scale == 1)"""
|
||||
|
||||
recipe_scale: NoneFloat = 1
|
||||
"""the number of times this recipe has been added"""
|
||||
|
||||
|
||||
class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRef):
|
||||
|
@ -79,6 +83,8 @@ class ShoppingListRecipeRefOut(MealieModel):
|
|||
shopping_list_id: UUID4
|
||||
recipe_id: UUID4
|
||||
recipe_quantity: float
|
||||
"""the number of times this recipe has been added"""
|
||||
|
||||
recipe: RecipeSummary
|
||||
|
||||
class Config:
|
||||
|
@ -118,6 +124,14 @@ class ShoppingListOut(ShoppingListUpdate):
|
|||
orm_mode = True
|
||||
|
||||
|
||||
class ShoppingListAddRecipeParams(MealieModel):
|
||||
recipe_increment_quantity: float = 1
|
||||
|
||||
|
||||
class ShoppingListRemoveRecipeParams(MealieModel):
|
||||
recipe_decrement_quantity: float = 1
|
||||
|
||||
|
||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary # noqa: E402
|
||||
from mealie.schema.recipe.recipe import RecipeSummary # noqa: E402
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import cast
|
||||
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.core.exceptions import UnexpectedNone
|
||||
|
@ -6,8 +8,10 @@ from mealie.schema.group import ShoppingListItemCreate, ShoppingListOut
|
|||
from mealie.schema.group.group_shopping_list import (
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemRecipeRef,
|
||||
ShoppingListItemRecipeRefOut,
|
||||
ShoppingListItemUpdate,
|
||||
)
|
||||
from mealie.schema.recipe import Recipe
|
||||
|
||||
|
||||
class ShoppingListService:
|
||||
|
@ -34,7 +38,7 @@ class ShoppingListService:
|
|||
units_not_none = not units_is_none
|
||||
units_equal = item1.unit_id == item2.unit_id
|
||||
|
||||
# Check if Notes are equal
|
||||
# Check if notes are equal
|
||||
if foods_is_none and units_is_none:
|
||||
return item1.note == item2.note
|
||||
|
||||
|
@ -48,7 +52,7 @@ class ShoppingListService:
|
|||
|
||||
def consolidate_list_items(self, item_list: list[ShoppingListItemOut]) -> list[ShoppingListItemOut]:
|
||||
"""
|
||||
itterates through the shopping list provided and returns
|
||||
iterates 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 accordingly.
|
||||
"""
|
||||
|
@ -64,17 +68,32 @@ class ShoppingListService:
|
|||
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 # type: ignore
|
||||
new_refs.append(ref)
|
||||
refs = {ref.recipe_id: ref for ref in base_item.recipe_references}
|
||||
for inner_ref in inner_item.recipe_references:
|
||||
if inner_ref.recipe_id not in refs:
|
||||
refs[inner_ref.recipe_id] = inner_ref
|
||||
|
||||
base_item.recipe_references.extend(new_refs)
|
||||
else:
|
||||
# merge recipe scales
|
||||
base_ref = refs[inner_ref.recipe_id]
|
||||
|
||||
# if the scale is missing we assume it's 1 for backwards compatibility
|
||||
# if the scale is 0 we leave it alone
|
||||
if base_ref.recipe_scale is None:
|
||||
base_ref.recipe_scale = 1
|
||||
|
||||
if inner_ref.recipe_scale is None:
|
||||
inner_ref.recipe_scale = 1
|
||||
|
||||
base_ref.recipe_scale += inner_ref.recipe_scale
|
||||
|
||||
base_item.recipe_references = list(refs.values())
|
||||
checked_items.append(inner_index)
|
||||
|
||||
consolidated_list.append(base_item)
|
||||
|
@ -111,7 +130,7 @@ class ShoppingListService:
|
|||
# Methods
|
||||
|
||||
def add_recipe_ingredients_to_list(
|
||||
self, list_id: UUID4, recipe_id: UUID4
|
||||
self, list_id: UUID4, recipe_id: UUID4, recipe_increment: float = 1
|
||||
) -> tuple[ShoppingListOut, list[ShoppingListItemOut], list[ShoppingListItemOut], list[ShoppingListItemOut]]:
|
||||
"""
|
||||
returns:
|
||||
|
@ -120,7 +139,7 @@ class ShoppingListService:
|
|||
- updated_shopping_list_items
|
||||
- deleted_shopping_list_items
|
||||
"""
|
||||
recipe = self.repos.recipes.get_one(recipe_id, "id")
|
||||
recipe: Recipe | None = self.repos.recipes.get_one(recipe_id, "id")
|
||||
if not recipe:
|
||||
raise UnexpectedNone("Recipe not found")
|
||||
|
||||
|
@ -150,14 +169,13 @@ class ShoppingListService:
|
|||
is_food=not recipe.settings.disable_amount if recipe.settings else False,
|
||||
food_id=food_id,
|
||||
unit_id=unit_id,
|
||||
quantity=ingredient.quantity,
|
||||
quantity=ingredient.quantity * recipe_increment if ingredient.quantity else 0,
|
||||
note=ingredient.note,
|
||||
label_id=label_id,
|
||||
recipe_id=recipe_id,
|
||||
recipe_references=[
|
||||
ShoppingListItemRecipeRef(
|
||||
recipe_id=recipe_id,
|
||||
recipe_quantity=ingredient.quantity,
|
||||
recipe_id=recipe_id, recipe_quantity=ingredient.quantity, recipe_scale=recipe_increment
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -176,13 +194,16 @@ class ShoppingListService:
|
|||
|
||||
not_found = True
|
||||
for refs in updated_shopping_list.recipe_references:
|
||||
if refs.recipe_id == recipe_id:
|
||||
refs.recipe_quantity += 1
|
||||
not_found = False
|
||||
if refs.recipe_id != recipe_id:
|
||||
continue
|
||||
|
||||
refs.recipe_quantity += recipe_increment
|
||||
not_found = False
|
||||
break
|
||||
|
||||
if not_found:
|
||||
updated_shopping_list.recipe_references.append(
|
||||
ShoppingListItemRecipeRef(recipe_id=recipe_id, recipe_quantity=1) # type: ignore
|
||||
ShoppingListItemRecipeRef(recipe_id=recipe_id, recipe_quantity=recipe_increment) # type: ignore
|
||||
)
|
||||
|
||||
updated_shopping_list = self.shopping_lists.update(updated_shopping_list.id, updated_shopping_list)
|
||||
|
@ -217,7 +238,7 @@ class ShoppingListService:
|
|||
return updated_shopping_list, new_shopping_list_items, updated_shopping_list_items, deleted_shopping_list_items
|
||||
|
||||
def remove_recipe_ingredients_from_list(
|
||||
self, list_id: UUID4, recipe_id: UUID4
|
||||
self, list_id: UUID4, recipe_id: UUID4, recipe_decrement: float = 1
|
||||
) -> tuple[ShoppingListOut, list[ShoppingListItemOut], list[ShoppingListItemOut]]:
|
||||
"""
|
||||
returns:
|
||||
|
@ -225,8 +246,8 @@ class ShoppingListService:
|
|||
- updated_shopping_list_items
|
||||
- deleted_shopping_list_items
|
||||
"""
|
||||
shopping_list = self.shopping_lists.get_one(list_id)
|
||||
|
||||
shopping_list = self.shopping_lists.get_one(list_id)
|
||||
if shopping_list is None:
|
||||
raise UnexpectedNone("Shopping list not found, cannot remove recipe ingredients")
|
||||
|
||||
|
@ -235,40 +256,63 @@ class ShoppingListService:
|
|||
for item in shopping_list.list_items:
|
||||
found = False
|
||||
|
||||
for ref in item.recipe_references:
|
||||
remove_qty: None | float = 0.0
|
||||
refs = cast(list[ShoppingListItemRecipeRefOut], item.recipe_references)
|
||||
for ref in refs:
|
||||
if ref.recipe_id != recipe_id:
|
||||
continue
|
||||
|
||||
if ref.recipe_id == recipe_id:
|
||||
self.list_item_refs.delete(ref.id) # type: ignore
|
||||
# if the scale is missing we assume it's 1 for backwards compatibility
|
||||
# if the scale is 0 we leave it alone
|
||||
if ref.recipe_scale is None:
|
||||
ref.recipe_scale = 1
|
||||
|
||||
# recipe quantity should never be None, but we check just in case
|
||||
if ref.recipe_quantity is None:
|
||||
ref.recipe_quantity = 0
|
||||
|
||||
# Set Quantity
|
||||
if ref.recipe_scale > recipe_decrement:
|
||||
# remove only part of the reference
|
||||
item.quantity -= recipe_decrement * ref.recipe_quantity
|
||||
|
||||
else:
|
||||
# remove everything that's left on the reference
|
||||
item.quantity -= ref.recipe_scale * ref.recipe_quantity
|
||||
|
||||
# Set Reference Scale
|
||||
ref.recipe_scale -= recipe_decrement
|
||||
if ref.recipe_scale <= 0:
|
||||
# delete the ref from the database and remove it from our list
|
||||
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
|
||||
found = True
|
||||
break
|
||||
|
||||
# If the item was found we need to check its new quantity
|
||||
if found:
|
||||
|
||||
if remove_qty is not None:
|
||||
item.quantity = item.quantity - remove_qty
|
||||
|
||||
if item.quantity <= 0:
|
||||
self.list_items.delete(item.id)
|
||||
deleted_shopping_list_items.append(item)
|
||||
|
||||
else:
|
||||
self.list_items.update(item.id, item)
|
||||
updated_shopping_list_items.append(item)
|
||||
|
||||
# Decrement the list recipe reference count
|
||||
for recipe_ref in shopping_list.recipe_references:
|
||||
if recipe_ref.recipe_id == recipe_id and recipe_ref.recipe_quantity is not None:
|
||||
recipe_ref.recipe_quantity -= 1.0
|
||||
if recipe_ref.recipe_id != recipe_id or recipe_ref.recipe_quantity is None:
|
||||
continue
|
||||
|
||||
if recipe_ref.recipe_quantity <= 0.0:
|
||||
self.list_refs.delete(recipe_ref.id)
|
||||
recipe_ref.recipe_quantity -= recipe_decrement
|
||||
|
||||
else:
|
||||
self.list_refs.update(recipe_ref.id, ref)
|
||||
break
|
||||
if recipe_ref.recipe_quantity <= 0.0:
|
||||
self.list_refs.delete(recipe_ref.id)
|
||||
|
||||
else:
|
||||
self.list_refs.update(recipe_ref.id, recipe_ref)
|
||||
|
||||
break
|
||||
|
||||
return (
|
||||
self.shopping_lists.get_one(shopping_list.id),
|
||||
|
|
|
@ -6,7 +6,7 @@ from mealie.schema.group.group_shopping_list import ShoppingListOut
|
|||
from mealie.schema.recipe.recipe import Recipe
|
||||
from tests import utils
|
||||
from tests.utils import api_routes
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.factories import random_int, random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
|
@ -119,6 +119,62 @@ def test_shopping_lists_add_recipe(
|
|||
assert refs[0]["recipeId"] == str(recipe.id)
|
||||
|
||||
|
||||
def test_shopping_list_add_recipe_scale(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
shopping_lists: list[ShoppingListOut],
|
||||
recipe_ingredient_only: Recipe,
|
||||
):
|
||||
sample_list = random.choice(shopping_lists)
|
||||
recipe = recipe_ingredient_only
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id), headers=unique_user.token
|
||||
)
|
||||
|
||||
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||
as_json = utils.assert_derserialize(response, 200)
|
||||
|
||||
assert len(as_json["recipeReferences"]) == 1
|
||||
assert as_json["recipeReferences"][0]["recipeQuantity"] == 1
|
||||
|
||||
for item in as_json["listItems"]:
|
||||
assert item["quantity"] == 1
|
||||
refs = item["recipeReferences"]
|
||||
|
||||
# only one reference per item
|
||||
assert len(refs) == 1
|
||||
|
||||
# base recipe quantity is 1
|
||||
assert refs[0]["recipeQuantity"] == 1
|
||||
|
||||
# scale was unspecified, which defaults to 1
|
||||
assert refs[0]["recipeScale"] == 1
|
||||
|
||||
recipe_scale = round(random.uniform(1, 10), 5)
|
||||
payload = {"recipeIncrementQuantity": recipe_scale}
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||
as_json = utils.assert_derserialize(response, 200)
|
||||
|
||||
assert len(as_json["recipeReferences"]) == 1
|
||||
assert as_json["recipeReferences"][0]["recipeQuantity"] == 1 + recipe_scale
|
||||
|
||||
for item in as_json["listItems"]:
|
||||
assert item["quantity"] == 1 + recipe_scale
|
||||
refs = item["recipeReferences"]
|
||||
|
||||
assert len(refs) == 1
|
||||
assert refs[0]["recipeQuantity"] == 1
|
||||
assert refs[0]["recipeScale"] == 1 + recipe_scale
|
||||
|
||||
|
||||
def test_shopping_lists_remove_recipe(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
|
@ -205,6 +261,129 @@ def test_shopping_lists_remove_recipe_multiple_quantity(
|
|||
assert refs[0]["recipeId"] == str(recipe.id)
|
||||
|
||||
|
||||
def test_shopping_list_remove_recipe_scale(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
shopping_lists: list[ShoppingListOut],
|
||||
recipe_ingredient_only: Recipe,
|
||||
):
|
||||
sample_list = random.choice(shopping_lists)
|
||||
recipe = recipe_ingredient_only
|
||||
|
||||
recipe_initital_scale = 100
|
||||
payload = {"recipeIncrementQuantity": recipe_initital_scale}
|
||||
|
||||
# first add a bunch of quantity to the list
|
||||
response = api_client.post(
|
||||
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||
as_json = utils.assert_derserialize(response, 200)
|
||||
|
||||
assert len(as_json["recipeReferences"]) == 1
|
||||
assert as_json["recipeReferences"][0]["recipeQuantity"] == recipe_initital_scale
|
||||
|
||||
for item in as_json["listItems"]:
|
||||
assert item["quantity"] == recipe_initital_scale
|
||||
refs = item["recipeReferences"]
|
||||
|
||||
assert len(refs) == 1
|
||||
assert refs[0]["recipeQuantity"] == 1
|
||||
assert refs[0]["recipeScale"] == recipe_initital_scale
|
||||
|
||||
recipe_decrement_scale = round(random.uniform(10, 90), 5)
|
||||
payload = {"recipeDecrementQuantity": recipe_decrement_scale}
|
||||
recipe_expected_scale = recipe_initital_scale - recipe_decrement_scale
|
||||
|
||||
# remove some of the recipes
|
||||
response = api_client.delete(
|
||||
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||
as_json = utils.assert_derserialize(response, 200)
|
||||
|
||||
assert len(as_json["recipeReferences"]) == 1
|
||||
assert as_json["recipeReferences"][0]["recipeQuantity"] == recipe_expected_scale
|
||||
|
||||
for item in as_json["listItems"]:
|
||||
assert item["quantity"] == recipe_expected_scale
|
||||
refs = item["recipeReferences"]
|
||||
|
||||
assert len(refs) == 1
|
||||
assert refs[0]["recipeQuantity"] == 1
|
||||
assert refs[0]["recipeScale"] == recipe_expected_scale
|
||||
|
||||
|
||||
def test_recipe_decrement_max(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
shopping_lists: list[ShoppingListOut],
|
||||
recipe_ingredient_only: Recipe,
|
||||
):
|
||||
sample_list = random.choice(shopping_lists)
|
||||
recipe = recipe_ingredient_only
|
||||
|
||||
recipe_scale = 10
|
||||
payload = {"recipeIncrementQuantity": recipe_scale}
|
||||
|
||||
# first add a bunch of quantity to the list
|
||||
response = api_client.post(
|
||||
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||
as_json = utils.assert_derserialize(response, 200)
|
||||
|
||||
assert len(as_json["recipeReferences"]) == 1
|
||||
assert as_json["recipeReferences"][0]["recipeQuantity"] == recipe_scale
|
||||
|
||||
for item in as_json["listItems"]:
|
||||
assert item["quantity"] == recipe_scale
|
||||
refs = item["recipeReferences"]
|
||||
|
||||
assert len(refs) == 1
|
||||
assert refs[0]["recipeQuantity"] == 1
|
||||
assert refs[0]["recipeScale"] == recipe_scale
|
||||
|
||||
# next add a little bit more of one item
|
||||
item_additional_quantity = random_int(1, 10)
|
||||
item_json = as_json["listItems"][0]
|
||||
item_json["quantity"] += item_additional_quantity
|
||||
|
||||
response = api_client.put(
|
||||
api_routes.groups_shopping_items_item_id(item["id"]), json=item_json, headers=unique_user.token
|
||||
)
|
||||
item_json = utils.assert_derserialize(response, 200)
|
||||
assert item_json["quantity"] == recipe_scale + item_additional_quantity
|
||||
|
||||
# now remove way too many instances of the recipe
|
||||
payload = {"recipeDecrementQuantity": recipe_scale * 100}
|
||||
response = api_client.delete(
|
||||
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
||||
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
|
||||
as_json = utils.assert_derserialize(response, 200)
|
||||
|
||||
# check that only the original recipe quantity and its reference were removed, not the additional quantity
|
||||
assert len(as_json["recipeReferences"]) == 0
|
||||
assert len(as_json["listItems"]) == 1
|
||||
|
||||
item = as_json["listItems"][0]
|
||||
assert item["quantity"] == item_additional_quantity
|
||||
assert len(item["recipeReferences"]) == 0
|
||||
|
||||
|
||||
def test_shopping_list_extras(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
|
|
|
@ -4,7 +4,7 @@ from mealie.core.config import get_app_settings
|
|||
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
|
||||
|
||||
ALEMBIC_VERSIONS = [
|
||||
{"version_num": "1923519381ad"},
|
||||
{"version_num": "167eb69066ad"},
|
||||
]
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue