From ee4ab41c1c58ebb97378822ec9e72b99166c1146 Mon Sep 17 00:00:00 2001 From: smilerz Date: Wed, 15 Dec 2021 07:24:12 -0600 Subject: [PATCH] test shoppingFood API --- cookbook/helper/shopping_helper.py | 24 ++--- cookbook/tests/api/test_api_food_shopping.py | 92 +++++++++++++++++++ .../tests/api/test_api_shopping_recipe.py | 18 ++-- cookbook/views/api.py | 7 ++ cookbook/views/views.py | 4 +- 5 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 cookbook/tests/api/test_api_food_shopping.py diff --git a/cookbook/helper/shopping_helper.py b/cookbook/helper/shopping_helper.py index 541fcb4f4..31a3f3cde 100644 --- a/cookbook/helper/shopping_helper.py +++ b/cookbook/helper/shopping_helper.py @@ -104,18 +104,18 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None x_ing = Ingredient.objects.filter(step__recipe=x, food__on_hand=False, space=space) else: x_ing = Ingredient.objects.filter(step__recipe=x, space=space) - for i in [x for x in x_ing if not x.food.ignore_shopping]: - ShoppingListEntry.objects.create( - list_recipe=list_recipe, - food=i.food, - unit=i.unit, - ingredient=i, - amount=i.amount * Decimal(servings_factor), - created_by=created_by, - space=space, - ) - # dont' add food to the shopping list that are actually recipes that will be added as ingredients - ingredients = ingredients.exclude(food__recipe=x) + for i in [x for x in x_ing if not x.food.ignore_shopping]: + ShoppingListEntry.objects.create( + list_recipe=list_recipe, + food=i.food, + unit=i.unit, + ingredient=i, + amount=i.amount * Decimal(servings_factor), + created_by=created_by, + space=space, + ) + # dont' add food to the shopping list that are actually recipes that will be added as ingredients + ingredients = ingredients.exclude(food__recipe=x) add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing if not append: diff --git a/cookbook/tests/api/test_api_food_shopping.py b/cookbook/tests/api/test_api_food_shopping.py new file mode 100644 index 000000000..39864effc --- /dev/null +++ b/cookbook/tests/api/test_api_food_shopping.py @@ -0,0 +1,92 @@ +# test create +# test create units +# test amounts +# test create wrong space +# test sharing +# test delete +# test delete checked (nothing should happen) +# test delete not shared (nothing happens) +# test delete shared + +import json + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scope, scopes_disabled + +from cookbook.models import Food, ShoppingListEntry +from cookbook.tests.factories import FoodFactory + +SHOPPING_LIST_URL = 'api:shoppinglistentry-list' +SHOPPING_FOOD_URL = 'api:food-shopping' + + +@pytest.fixture() +def food(request, space_1, u1_s1): + return FoodFactory(space=space_1) + + +def test_shopping_forbidden_methods(food, u1_s1): + r = u1_s1.post( + reverse(SHOPPING_FOOD_URL, args={food.id})) + assert r.status_code == 405 + + r = u1_s1.delete( + reverse(SHOPPING_FOOD_URL, args={food.id})) + assert r.status_code == 405 + + r = u1_s1.get( + reverse(SHOPPING_FOOD_URL, args={food.id})) + assert r.status_code == 405 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 204], + ['u1_s2', 404], + ['a1_s1', 204], +]) +def test_shopping_food_create(request, arg, food): + c = request.getfixturevalue(arg[0]) + r = c.put(reverse(SHOPPING_FOOD_URL, args={food.id})) + assert r.status_code == arg[1] + if r.status_code == 204: + assert len(json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)) == 1 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 403], + ['u1_s1', 204], + ['u1_s2', 404], + ['a1_s1', 204], +]) +def test_shopping_food_delete(request, arg, food): + c = request.getfixturevalue(arg[0]) + r = c.put( + reverse(SHOPPING_FOOD_URL, args={food.id}), + {'_delete': "true"}, + content_type='application/json' + ) + assert r.status_code == arg[1] + if r.status_code == 204: + assert len(json.loads(c.get(reverse(SHOPPING_LIST_URL)).content)) == 0 + + +def test_shopping_food_share(u1_s1, u2_s1, food, space_1): + with scope(space=space_1): + food2 = FoodFactory(space=space_1) + r = u1_s1.put(reverse(SHOPPING_FOOD_URL, args={food.id})) + r = u2_s1.put(reverse(SHOPPING_FOOD_URL, args={food.id})) + assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 1 + assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 1 + + with scopes_disabled(): + user = auth.get_user(u1_s1) + user2 = auth.get_user(u2_s1) + user.userpreference.shopping_share.add(user2) + user.userpreference.save() + assert len(json.loads(u1_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 1 + assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 2 diff --git a/cookbook/tests/api/test_api_shopping_recipe.py b/cookbook/tests/api/test_api_shopping_recipe.py index 5b137fed0..821421d87 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 Food, Ingredient, ShoppingListEntry +from cookbook.models import Food, Ingredient, ShoppingListEntry, Step from cookbook.tests.factories import MealPlanFactory, RecipeFactory, StepFactory, UserFactory SHOPPING_LIST_URL = 'api:shoppinglistentry-list' @@ -176,8 +176,9 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u @pytest.mark.parametrize("user2, sle_count", [ ({'mealplan_autoadd_shopping': False}, (0, 17)), - ({'mealplan_autoinclude_related': False}, (7, 7)), + ({'mealplan_autoinclude_related': False}, (8, 8)), ({'mealplan_autoexclude_onhand': False}, (19, 19)), + ({'mealplan_autoexclude_onhand': False, 'mealplan_autoinclude_related': False}, (9, 9)), ], indirect=['user2']) @pytest.mark.parametrize("use_mealplan", [(False), (True), ]) @pytest.mark.parametrize("recipe", [({'steps__recipe_count': 1})], indirect=['recipe']) @@ -186,13 +187,14 @@ def test_shopping_recipe_userpreference(recipe, sle_count, use_mealplan, user2): user = auth.get_user(user2) # setup recipe with 10 ingredients, 1 step recipe with 10 ingredients, 2 food onhand(from recipe and step_recipe), 1 food ignore shopping ingredients = Ingredient.objects.filter(step__recipe=recipe) - food = Food.objects.get(id=ingredients[0].food.id) - food.on_hand = True - food.save() - food = Step.objects.filter(type=Step.RECIPE).first().ingredients.first() - food.on_hand = True - food.save() food = Food.objects.get(id=ingredients[2].food.id) + food.on_hand = True + food.save() + food = recipe.steps.filter(type=Step.RECIPE).first().step_recipe.steps.first().ingredients.first().food + food = Food.objects.get(id=food.id) + food.on_hand = True + food.save() + food = Food.objects.get(id=ingredients[4].food.id) food.ignore_shopping = True food.save() diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 233b2d91d..e54456f58 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -413,7 +413,10 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin): pagination_class = DefaultPagination @decorators.action(detail=True, methods=['PUT'], serializer_class=FoodShoppingUpdateSerializer,) + # TODO DRF only allows one action in a decorator action without overriding get_operation_id_base() this should be PUT and DELETE probably def shopping(self, request, pk): + if self.request.space.demo: + raise PermissionDenied(detail='Not available in demo', code=None) obj = self.get_object() shared_users = list(self.request.user.get_shopping_share()) shared_users.append(request.user) @@ -652,12 +655,16 @@ class RecipeViewSet(viewsets.ModelViewSet): return Response(serializer.data) return Response(serializer.errors, 400) + # TODO: refactor API to use post/put/delete or leave as put and change VUE to use list_recipe after creating + # DRF only allows one action in a decorator action without overriding get_operation_id_base() @decorators.action( detail=True, methods=['PUT'], serializer_class=RecipeShoppingUpdateSerializer, ) def shopping(self, request, pk): + if self.request.space.demo: + raise PermissionDenied(detail='Not available in demo', code=None) obj = self.get_object() ingredients = request.data.get('ingredients', None) servings = request.data.get('servings', None) diff --git a/cookbook/views/views.py b/cookbook/views/views.py index ec1f61bcd..ef79e072c 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -254,13 +254,13 @@ def latest_shopping_list(request): @group_required('user') -def shopping_list(request, pk=None): +def shopping_list(request, pk=None): # TODO deprecate html_list = request.GET.getlist('r') recipes = [] for r in html_list: r = r.replace('[', '').replace(']', '') - if re.match(r'^([0-9])+,([0-9])+[.]*([0-9])*$', r): + if re.match(r'^([0-9])+,([0-9])+[.]*([0-9])*$', r): # vulnerable to DoS rid, multiplier = r.split(',') if recipe := Recipe.objects.filter(pk=int(rid), space=request.space).first(): recipes.append({'recipe': recipe.id, 'multiplier': multiplier})