diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py index 8d76352c0..154cb4da9 100644 --- a/cookbook/helper/shopping_helper.py +++ b/cookbook/helper/shopping_helper.py @@ -52,12 +52,14 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None if not r: raise ValueError(_("You must supply a recipe or mealplan")) - created_by = created_by or getattr(mealplan, 'created_by', None) or getattr(list_recipe, 'created_by', None) if not created_by: raise ValueError(_("You must supply a created_by")) - if type(servings) not in [int, float]: + try: + servings = float(servings) + except ValueError: servings = getattr(mealplan, 'servings', 1.0) + servings_factor = servings / r.servings shared_users = list(created_by.get_shopping_share()) @@ -68,6 +70,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings) created = True + related_step_ing = [] if servings == 0 and not created: list_recipe.delete() return [] @@ -79,7 +82,6 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand: ingredients = ingredients.exclude(food__on_hand=True) - related_step_ing = [] if related := created_by.userpreference.mealplan_autoinclude_related: # TODO: add levels of related recipes to use when auto-adding mealplans related_recipes = r.get_related_recipes() @@ -128,6 +130,8 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None if not created and list_recipe.servings != servings: update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True)) + list_recipe.servings = servings + list_recipe.save() for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients): sle.amount = sle.ingredient.amount * Decimal(servings_factor) sle.save() diff --git a/cookbook/tests/api/test_api_shopping_recipe.py b/cookbook/tests/api/test_api_shopping_recipe.py index 9c8447807..50bdaa84e 100644 --- a/cookbook/tests/api/test_api_shopping_recipe.py +++ b/cookbook/tests/api/test_api_shopping_recipe.py @@ -10,7 +10,7 @@ from django.utils import timezone from django_scopes import scopes_disabled from pytest_factoryboy import LazyFixture, register -from cookbook.models import ShoppingListEntry +from cookbook.models import Ingredient, ShoppingListEntry from cookbook.tests.factories import RecipeFactory SHOPPING_LIST_URL = 'api:shoppinglistentry-list' @@ -82,7 +82,69 @@ def test_shopping_recipe_method(request, arg, recipe, sle_count, u1_s1, u2_s1): assert r.status_code == 405 +@pytest.mark.parametrize("recipe, sle_count", [ + ({}, 10), + ({'steps__recipe_count': 1}, 20), # shopping list from recipe with StepRecipe + ({'steps__food_recipe_count': {'step': 0, 'count': 1}}, 19), # shopping list from recipe with food recipe + ({'steps__food_recipe_count': {'step': 0, 'count': 1}, 'steps__recipe_count': 1}, 29), # shopping list from recipe with StepRecipe and food recipe +], indirect=['recipe']) +def test_shopping_recipe_edit(request, recipe, sle_count, u1_s1, u2_s1): + with scopes_disabled(): # TODO take out + + user = auth.get_user(u1_s1) + user2 = auth.get_user(u2_s1) + user.userpreference.mealplan_autoinclude_related = True + user.userpreference.shopping_share.add(user2) + user.userpreference.save() + + r = u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id})) + r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) + assert [x['created_by']['id'] for x in r].count(user.id) == sle_count + all_ing = [x['ingredient'] for x in r] + keep_ing = [x['ingredient'] for x in r[2:]] # TODO change this to remove 1 from each third + list_recipe = r[0]['list_recipe'] + amount_sum = sum([x['amount'] for x in r]) + + # test modifying shopping list as different user + # test increasing servings size of recipe shopping list + u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), + {'list_recipe': list_recipe, 'servings': 2*recipe.servings}, + content_type='application/json' + ) + r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) + assert sum([x['amount'] for x in r]) == amount_sum * 2 + assert len(r) == sle_count + assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count + + # testing decreasing servings size of recipe shopping list + u1_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), + {'list_recipe': list_recipe, 'servings': .5 * recipe.servings}, + content_type='application/json' + ) + r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) + assert sum([x['amount'] for x in r]) == amount_sum * .5 + assert len(r) == sle_count + assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count + + # test removing 2 items from shopping list + u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), + {'list_recipe': list_recipe, 'ingredients': keep_ing}, + content_type='application/json' + ) + r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) + assert len(r) == sle_count - 2 + assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count - 2 + + # add all ingredients to existing shopping list - don't change serving size + u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}), + {'list_recipe': list_recipe, 'ingredients': all_ing}, + content_type='application/json' + ) + r = json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content) + assert sum([x['amount'] for x in r]) == amount_sum * .5 + assert len(r) == sle_count + assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count + + # TODO test creating shopping list from recipe that includes recipes from multiple users -# TODO test create shopping list from recipe, excluding ingredients # TODO meal plan recipe with all the user preferences tested -# TODO shopping list from recipe with different servings diff --git a/cookbook/views/api.py b/cookbook/views/api.py index a96825f96..c633b90a2 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -661,10 +661,15 @@ class RecipeViewSet(viewsets.ModelViewSet): def shopping(self, request, pk): obj = self.get_object() ingredients = request.data.get('ingredients', None) - servings = request.data.get('servings', obj.servings) - list_recipe = request.data.get('list_recipe', None) + servings = request.data.get('servings', None) + list_recipe = ShoppingListRecipe.objects.filter(id=request.data.get('list_recipe', None)).first() + if servings is None: + servings = getattr(list_recipe, 'servings', obj.servings) + # created_by needs to be sticky to original creator as it is 'their' shopping list + # changing shopping list created_by can shift some items to new owner which may not share in the other direction + created_by = getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', request.user) content = {'msg': _(f'{obj.name} was added to the shopping list.')} - list_from_recipe(list_recipe=list_recipe, recipe=obj, ingredients=ingredients, servings=servings, space=request.space, created_by=request.user) + list_from_recipe(list_recipe=list_recipe, recipe=obj, ingredients=ingredients, servings=servings, space=request.space, created_by=created_by) return Response(content, status=status.HTTP_204_NO_CONTENT)