mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-25 08:09:41 +02:00
fix: Bulk Add Recipes to Shopping List (#5054)
This commit is contained in:
parent
3d1b76bcad
commit
716c85cc3b
13 changed files with 306 additions and 77 deletions
|
@ -6,7 +6,6 @@ import pytest
|
|||
from fastapi.testclient import TestClient
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
|
||||
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood
|
||||
from tests import utils
|
||||
|
|
|
@ -3,6 +3,7 @@ import random
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from mealie.schema.household.group_shopping_list import (
|
||||
ShoppingListAddRecipeParamsBulk,
|
||||
ShoppingListItemOut,
|
||||
ShoppingListItemUpdate,
|
||||
ShoppingListItemUpdateBulk,
|
||||
|
@ -171,6 +172,78 @@ def test_shopping_lists_add_recipe(
|
|||
assert refs[0]["recipeQuantity"] == 2
|
||||
|
||||
|
||||
def test_shopping_lists_add_recipes(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
shopping_lists: list[ShoppingListOut],
|
||||
recipes_ingredient_only: list[Recipe],
|
||||
):
|
||||
sample_list = random.choice(shopping_lists)
|
||||
recipes = recipes_ingredient_only
|
||||
all_ingredients = [ingredient for recipe in recipes for ingredient in recipe.recipe_ingredient]
|
||||
|
||||
recipes_post_data = utils.jsonify(
|
||||
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump() for recipe in recipes]
|
||||
)
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=recipes_post_data,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# get list and verify items against ingredients
|
||||
response = api_client.get(
|
||||
api_routes.households_shopping_lists_item_id(sample_list.id),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
as_json = utils.assert_deserialize(response, 200)
|
||||
assert len(as_json["listItems"]) == len(all_ingredients)
|
||||
|
||||
known_ingredients = {ingredient.note: ingredient for ingredient in all_ingredients}
|
||||
for item in as_json["listItems"]:
|
||||
assert item["note"] in known_ingredients
|
||||
|
||||
ingredient = known_ingredients[item["note"]]
|
||||
assert item["quantity"] == (ingredient.quantity or 0)
|
||||
|
||||
# check recipe reference was added with quantity 1
|
||||
refs = as_json["recipeReferences"]
|
||||
assert len(refs) == len(recipes)
|
||||
refs_by_id = {ref["recipeId"]: ref for ref in refs}
|
||||
for recipe in recipes:
|
||||
assert str(recipe.id) in refs_by_id
|
||||
assert refs_by_id[str(recipe.id)]["recipeQuantity"] == 1
|
||||
|
||||
# add the recipes again and check the resulting items
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=recipes_post_data,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = api_client.get(
|
||||
api_routes.households_shopping_lists_item_id(sample_list.id),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
as_json = utils.assert_deserialize(response, 200)
|
||||
assert len(as_json["listItems"]) == len(all_ingredients)
|
||||
|
||||
for item in as_json["listItems"]:
|
||||
assert item["note"] in known_ingredients
|
||||
|
||||
ingredient = known_ingredients[item["note"]]
|
||||
assert item["quantity"] == (ingredient.quantity or 0) * 2
|
||||
|
||||
refs = as_json["recipeReferences"]
|
||||
assert len(refs) == len(recipes)
|
||||
refs_by_id = {ref["recipeId"]: ref for ref in refs}
|
||||
for recipe in recipes:
|
||||
assert str(recipe.id) in refs_by_id
|
||||
assert refs_by_id[str(recipe.id)]["recipeQuantity"] == 2
|
||||
|
||||
|
||||
def test_shopping_lists_add_one_with_zero_quantity(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
|
@ -209,7 +282,8 @@ def test_shopping_lists_add_one_with_zero_quantity(
|
|||
|
||||
# add the recipe to the list and make sure there are three list items
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
|
@ -242,16 +316,19 @@ def test_shopping_lists_add_custom_recipe_items(
|
|||
recipe = recipe_ingredient_only
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
custom_items = random.sample(recipe_ingredient_only.recipe_ingredient, k=3)
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=utils.jsonify(
|
||||
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_ingredients=custom_items).model_dump()]
|
||||
),
|
||||
headers=unique_user.token,
|
||||
json={"recipeIngredients": utils.jsonify(custom_items)},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
@ -291,7 +368,8 @@ def test_shopping_list_ref_removes_itself(
|
|||
# add a recipe to a list, then check off all recipe items and make sure the recipe ref is deleted
|
||||
recipe = recipe_ingredient_only
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
utils.assert_deserialize(response, 200)
|
||||
|
@ -365,7 +443,8 @@ def test_shopping_lists_add_recipe_with_merge(
|
|||
|
||||
# add the recipe to the list and make sure there are only three list items, and their quantities/refs are correct
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
|
@ -403,6 +482,85 @@ def test_shopping_lists_add_recipe_with_merge(
|
|||
assert all([found_item_1, found_item_2, found_duplicate_item])
|
||||
|
||||
|
||||
def test_shopping_lists_add_recipes_with_merge(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
shopping_lists: list[ShoppingListOut],
|
||||
):
|
||||
shopping_list = random.choice(shopping_lists)
|
||||
|
||||
# build two recipes that share an ingredient
|
||||
recipes: list[Recipe] = []
|
||||
common_note = random_string()
|
||||
for _ in range(2):
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
|
||||
recipe_slug = utils.assert_deserialize(response, 201)
|
||||
|
||||
response = api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token)
|
||||
recipe_data = utils.assert_deserialize(response, 200)
|
||||
|
||||
ingredient_unique = {"quantity": 1, "note": random_string()}
|
||||
ingredient_duplicate = {"quantity": 1, "note": common_note}
|
||||
|
||||
recipe_data["recipeIngredient"] = [ingredient_unique, ingredient_duplicate]
|
||||
response = api_client.put(
|
||||
f"{api_routes.recipes}/{recipe_slug}",
|
||||
json=recipe_data,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
utils.assert_deserialize(response, 200)
|
||||
|
||||
recipe = Recipe.model_validate_json(
|
||||
api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token).content
|
||||
)
|
||||
assert recipe.id
|
||||
assert len(recipe.recipe_ingredient) == 2
|
||||
recipes.append(recipe)
|
||||
|
||||
# add the recipes to the list and make sure there are only five list items, and their quantities/refs are correct
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump() for recipe in recipes]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
response = api_client.get(
|
||||
api_routes.households_shopping_lists_item_id(shopping_list.id),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
shopping_list_out = ShoppingListOut.model_validate(utils.assert_deserialize(response, 200))
|
||||
|
||||
assert len(shopping_list_out.list_items) == 3
|
||||
|
||||
found_recipe_1_item = False
|
||||
found_recipe_2_item = False
|
||||
found_duplicate_item = False
|
||||
for list_item in shopping_list_out.list_items:
|
||||
if list_item.note == common_note:
|
||||
assert list_item.quantity == 2
|
||||
assert len(list_item.recipe_references) == 2
|
||||
assert {ref.recipe_id for ref in list_item.recipe_references} == {recipe.id for recipe in recipes}
|
||||
found_duplicate_item = True
|
||||
|
||||
else:
|
||||
assert len(list_item.recipe_references) == 1
|
||||
assert list_item.quantity == 1
|
||||
if list_item.note == recipes[0].recipe_ingredient[0].note:
|
||||
assert list_item.recipe_references[0].recipe_id == recipes[0].id
|
||||
found_recipe_1_item = True
|
||||
elif list_item.note == recipes[1].recipe_ingredient[0].note:
|
||||
assert list_item.recipe_references[0].recipe_id == recipes[1].id
|
||||
found_recipe_2_item = True
|
||||
else:
|
||||
raise Exception("Unexpected item")
|
||||
|
||||
for ref in list_item.recipe_references:
|
||||
assert ref.recipe_scale == 1
|
||||
assert ref.recipe_quantity == 1
|
||||
|
||||
assert all([found_recipe_1_item, found_recipe_2_item, found_duplicate_item])
|
||||
|
||||
|
||||
def test_shopping_list_add_recipe_scale(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
|
@ -413,7 +571,8 @@ def test_shopping_list_add_recipe_scale(
|
|||
recipe = recipe_ingredient_only
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
|
@ -440,10 +599,12 @@ def test_shopping_list_add_recipe_scale(
|
|||
assert refs[0]["recipeScale"] == 1
|
||||
|
||||
recipe_scale = round(random.uniform(1, 10), 5)
|
||||
payload = {"recipeIncrementQuantity": recipe_scale}
|
||||
payload = utils.jsonify(
|
||||
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_increment_quantity=recipe_scale).model_dump()]
|
||||
)
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
@ -476,9 +637,11 @@ def test_shopping_lists_remove_recipe(
|
|||
recipe = recipe_ingredient_only
|
||||
|
||||
# add two instances of the recipe
|
||||
payload = {"recipeIncrementQuantity": 2}
|
||||
payload = utils.jsonify(
|
||||
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_increment_quantity=2).model_dump()]
|
||||
)
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=payload,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
@ -539,7 +702,8 @@ def test_shopping_lists_remove_recipe_multiple_quantity(
|
|||
|
||||
for _ in range(3):
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
@ -592,11 +756,17 @@ def test_shopping_list_remove_recipe_scale(
|
|||
recipe = recipe_ingredient_only
|
||||
|
||||
recipe_initital_scale = 100
|
||||
payload: dict = {"recipeIncrementQuantity": recipe_initital_scale}
|
||||
payload = utils.jsonify(
|
||||
[
|
||||
ShoppingListAddRecipeParamsBulk(
|
||||
recipe_id=recipe.id, recipe_increment_quantity=recipe_initital_scale
|
||||
).model_dump()
|
||||
]
|
||||
)
|
||||
|
||||
# first add a bunch of quantity to the list
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
@ -657,11 +827,13 @@ def test_recipe_decrement_max(
|
|||
recipe = recipe_ingredient_only
|
||||
|
||||
recipe_scale = 10
|
||||
payload = {"recipeIncrementQuantity": recipe_scale}
|
||||
payload = utils.jsonify(
|
||||
[ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id, recipe_increment_quantity=recipe_scale).model_dump()]
|
||||
)
|
||||
|
||||
# first add a bunch of quantity to the list
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(sample_list.id),
|
||||
headers=unique_user.token,
|
||||
json=payload,
|
||||
)
|
||||
|
@ -757,13 +929,15 @@ def test_recipe_manipulation_with_zero_quantities(
|
|||
|
||||
# add the recipe to the list twice and make sure the quantity is still zero
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
utils.assert_deserialize(response, 200)
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.households_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
|
||||
api_routes.households_shopping_lists_item_id_recipe(shopping_list.id),
|
||||
json=utils.jsonify([ShoppingListAddRecipeParamsBulk(recipe_id=recipe.id).model_dump()]),
|
||||
headers=unique_user.token,
|
||||
)
|
||||
utils.assert_deserialize(response, 200)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue