1
0
Fork 0
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:
Michael Genson 2025-02-27 07:58:40 -06:00 committed by GitHub
parent 3d1b76bcad
commit 716c85cc3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 306 additions and 77 deletions

View file

@ -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

View file

@ -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)