From 641feede74c025034ac9e1a39d9651b06bc64018 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 17 Mar 2021 19:55:34 +0100 Subject: [PATCH] space nested serializers --- cookbook/models.py | 40 ++++-- cookbook/serializer.py | 16 ++- cookbook/tests/api/test_api_shopping_list.py | 48 ------- .../pytest/api/test_api_shopping_list.py | 120 ++++++++++++++++++ cookbook/views/api.py | 40 +++--- 5 files changed, 186 insertions(+), 78 deletions(-) delete mode 100644 cookbook/tests/api/test_api_shopping_list.py create mode 100644 cookbook/tests/pytest/api/test_api_shopping_list.py diff --git a/cookbook/models.py b/cookbook/models.py index acfdb7ab8..e0935a392 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -30,6 +30,14 @@ def get_model_name(model): class PermissionModelMixin: + + @staticmethod + def get_space_key(): + return ('space',) + + def get_space_kwarg(self): + return '__'.join(self.get_space_key()) + def get_owner(self): if getattr(self, 'created_by', None): return self.created_by @@ -38,8 +46,9 @@ class PermissionModelMixin: return None def get_space(self): - if getattr(self, 'space', None): - return self.space + p = '.'.join(self.get_space_key()) + if getattr(self, p, None): + return getattr(self, p) raise NotImplementedError('get space for method not implemented and standard fields not available') @@ -207,8 +216,9 @@ class SupermarketCategoryRelation(models.Model, PermissionModelMixin): objects = ScopedManager(space='supermarket__space') - def get_space(self): - return self.supermarket.space + @staticmethod + def get_space_key(): + return 'supermarket', 'space' class Meta: ordering = ('order',) @@ -288,6 +298,10 @@ class Ingredient(models.Model, PermissionModelMixin): objects = ScopedManager(space='step__recipe__space') + @staticmethod + def get_space_key(): + return 'step', 'recipe', 'space' + def get_space(self): return self.step_set.first().recipe_set.first().space @@ -316,6 +330,10 @@ class Step(models.Model, PermissionModelMixin): objects = ScopedManager(space='recipe__space') + @staticmethod + def get_space_key(): + return 'recipe', 'space' + def get_space(self): return self.recipe_set.first().space @@ -340,6 +358,10 @@ class NutritionInformation(models.Model, PermissionModelMixin): objects = ScopedManager(space='recipe__space') + @staticmethod + def get_space_key(): + return 'recipe', 'space' + def get_space(self): return self.recipe_set.first().space @@ -388,8 +410,9 @@ class Comment(models.Model, PermissionModelMixin): objects = ScopedManager(space='recipe__space') - def get_space(self): - return self.recipe.space + @staticmethod + def get_space_key(): + return 'recipe', 'space' def __str__(self): return self.text @@ -429,8 +452,9 @@ class RecipeBookEntry(models.Model, PermissionModelMixin): objects = ScopedManager(space='book__space') - def get_space(self): - return self.book.space + @staticmethod + def get_space_key(): + return 'book', 'space' def __str__(self): return self.recipe.name diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 985aa03b2..116329b01 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -41,6 +41,16 @@ class CustomDecimalField(serializers.Field): raise ValidationError('A valid number is required') +class SpaceFilterSerializer(serializers.ListSerializer): + + def to_representation(self, data): + if self.child.Meta.model == User: + data = data.filter(userpreference__space=self.context['request'].space) + else: + data = data.filter(**{'__'.join(data.model.get_space_key()): self.context['request'].space}) + return super().to_representation(data) + + class SpacedModelSerializer(serializers.ModelSerializer): def create(self, validated_data): validated_data['space'] = self.context['request'].space @@ -54,6 +64,7 @@ class MealTypeSerializer(SpacedModelSerializer): return super().create(validated_data) class Meta: + list_serializer_class = SpaceFilterSerializer model = MealType fields = ('id', 'name', 'order', 'created_by') read_only_fields = ('created_by',) @@ -66,6 +77,7 @@ class UserNameSerializer(WritableNestedModelSerializer): return obj.get_user_name() class Meta: + list_serializer_class = SpaceFilterSerializer model = User fields = ('id', 'username') @@ -129,6 +141,7 @@ class KeywordLabelSerializer(serializers.ModelSerializer): return str(obj) class Meta: + list_serializer_class = SpaceFilterSerializer model = Keyword fields = ( 'id', 'label', @@ -383,6 +396,7 @@ class ShoppingListSerializer(WritableNestedModelSerializer): def create(self, validated_data): validated_data['space'] = self.context['request'].space + validated_data['created_by'] = self.context['request'].user return super().create(validated_data) class Meta: @@ -391,7 +405,7 @@ class ShoppingListSerializer(WritableNestedModelSerializer): 'id', 'uuid', 'note', 'recipes', 'entries', 'shared', 'finished', 'supermarket', 'created_by', 'created_at' ) - read_only_fields = ('id',) + read_only_fields = ('id', 'created_by',) class ShoppingListAutoSyncSerializer(WritableNestedModelSerializer): diff --git a/cookbook/tests/api/test_api_shopping_list.py b/cookbook/tests/api/test_api_shopping_list.py deleted file mode 100644 index e25a16a5a..000000000 --- a/cookbook/tests/api/test_api_shopping_list.py +++ /dev/null @@ -1,48 +0,0 @@ -from cookbook.models import ShoppingList -from cookbook.tests.views.test_views import TestViews -from django.contrib import auth -from django.urls import reverse - - -class TestApiShopping(TestViews): - - def setUp(self): - super(TestApiShopping, self).setUp() - self.list_1 = ShoppingList.objects.create( - created_by=auth.get_user(self.user_client_1) - ) - self.list_2 = ShoppingList.objects.create( - created_by=auth.get_user(self.user_client_2) - ) - - def test_shopping_view_permissions(self): - self.batch_requests( - [ - (self.anonymous_client, 403), - (self.guest_client_1, 404), - (self.user_client_1, 200), - (self.user_client_2, 404), - (self.admin_client_1, 404), - (self.superuser_client, 200) - ], - reverse( - 'api:shoppinglist-detail', args={self.list_1.id} - ) - ) - - self.list_1.shared.add(auth.get_user(self.user_client_2)) - - self.batch_requests( - [ - (self.anonymous_client, 403), - (self.guest_client_1, 404), - (self.user_client_1, 200), - (self.user_client_2, 200), - (self.admin_client_1, 404), - (self.superuser_client, 200) - ], - reverse( - 'api:shoppinglist-detail', args={self.list_1.id}) - ) - - # TODO add tests for editing diff --git a/cookbook/tests/pytest/api/test_api_shopping_list.py b/cookbook/tests/pytest/api/test_api_shopping_list.py new file mode 100644 index 000000000..25ba175aa --- /dev/null +++ b/cookbook/tests/pytest/api/test_api_shopping_list.py @@ -0,0 +1,120 @@ +import json + +import pytest +from django.contrib import auth +from django.urls import reverse +from django_scopes import scopes_disabled + +from cookbook.models import RecipeBook, Storage, Sync, SyncLog, ShoppingList + +LIST_URL = 'api:shoppinglist-list' +DETAIL_URL = 'api:shoppinglist-detail' + + +@pytest.fixture() +def obj_1(space_1, u1_s1): + return ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, ) + + +@pytest.fixture +def obj_2(space_1, u1_s1): + return ShoppingList.objects.create(created_by=auth.get_user(u1_s1), space=space_1, ) + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 200], + ['u1_s1', 200], + ['a1_s1', 200], +]) +def test_list_permission(arg, request): + c = request.getfixturevalue(arg[0]) + assert c.get(reverse(LIST_URL)).status_code == arg[1] + + +def test_list_space(obj_1, obj_2, u1_s1, u1_s2, space_2): + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + + obj_1.space = space_2 + obj_1.save() + + assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1 + assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0 + + +def test_share(obj_1, u1_s1, u2_s1, u1_s2): + assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200 + assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404 + assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404 + + obj_1.shared.add(auth.get_user(u2_s1)) + obj_1.shared.add(auth.get_user(u1_s2)) + + assert u1_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200 + assert u2_s1.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 200 + assert u1_s2.get(reverse(DETAIL_URL, args={obj_1.id})).status_code == 404 + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 404], + ['u1_s1', 200], + ['a1_s1', 404], + ['g1_s2', 404], + ['u1_s2', 404], + ['a1_s2', 404], +]) +def test_update(arg, request, obj_1): + c = request.getfixturevalue(arg[0]) + r = c.patch( + reverse( + DETAIL_URL, + args={obj_1.id} + ), + {'note': 'new'}, + content_type='application/json' + ) + assert r.status_code == arg[1] + if r.status_code == 200: + response = json.loads(r.content) + assert response['note'] == 'new' + + +@pytest.mark.parametrize("arg", [ + ['a_u', 403], + ['g1_s1', 201], + ['u1_s1', 201], + ['a1_s1', 201], +]) +def test_add(arg, request): + c = request.getfixturevalue(arg[0]) + r = c.post( + reverse(LIST_URL), + {'note': 'test', 'recipes': [], 'shared': [], 'entries': [], 'supermarket': None}, + content_type='application/json' + ) + response = json.loads(r.content) + print(r.content) + assert r.status_code == arg[1] + if r.status_code == 201: + assert response['note'] == 'test' + + +def test_delete(u1_s1, u1_s2, obj_1): + r = u1_s2.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + assert r.status_code == 404 + + r = u1_s1.delete( + reverse( + DETAIL_URL, + args={obj_1.id} + ) + ) + + assert r.status_code == 204 diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 301be9ec9..173b4a95b 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -91,7 +91,7 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet): http_method_names = ['get'] def get_queryset(self): - queryset = self.queryset.filter(userpreference__space=self.request.user.userpreference.space) + queryset = self.queryset.filter(userpreference__space=self.request.space) try: filter_list = self.request.query_params.get('filter_list', None) if filter_list is not None: @@ -118,7 +118,7 @@ class StorageViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsAdmin, ] def get_queryset(self): - return self.queryset.filter(space=self.request.user.userpreference.space) + return self.queryset.filter(space=self.request.space) class SyncViewSet(viewsets.ModelViewSet): @@ -127,7 +127,7 @@ class SyncViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsAdmin, ] def get_queryset(self): - return self.queryset.filter(space=self.request.user.userpreference.space) + return self.queryset.filter(space=self.request.space) class SyncLogViewSet(viewsets.ReadOnlyModelViewSet): @@ -136,7 +136,7 @@ class SyncLogViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [CustomIsAdmin, ] def get_queryset(self): - return self.queryset.filter(sync__space=self.request.user.userpreference.space) + return self.queryset.filter(sync__space=self.request.space) class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin): @@ -145,7 +145,7 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin): permission_classes = [CustomIsUser] def get_queryset(self): - self.queryset = self.queryset.filter(space=self.request.user.userpreference.space) + self.queryset = self.queryset.filter(space=self.request.space) return super().get_queryset() @@ -163,7 +163,7 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin): permission_classes = [CustomIsUser] def get_queryset(self): - self.queryset = self.queryset.filter(space=self.request.user.userpreference.space) + self.queryset = self.queryset.filter(space=self.request.space) return super().get_queryset() @@ -173,7 +173,7 @@ class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin): permission_classes = [CustomIsUser] def get_queryset(self): - self.queryset = self.queryset.filter(space=self.request.user.userpreference.space) + self.queryset = self.queryset.filter(space=self.request.space) return super().get_queryset() @@ -183,7 +183,7 @@ class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin): permission_classes = [CustomIsUser] def get_queryset(self): - self.queryset = self.queryset.filter(space=self.request.user.userpreference.space) + self.queryset = self.queryset.filter(space=self.request.space) return super().get_queryset() @@ -193,7 +193,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin): permission_classes = [CustomIsOwner] def get_queryset(self): - self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space) + self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space) return super().get_queryset() @@ -203,7 +203,7 @@ class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet): permission_classes = [CustomIsOwner] def get_queryset(self): - return self.queryset.filter(book__created_by=self.request.user).filter(book__space=self.request.user.userpreference.space) + return self.queryset.filter(book__created_by=self.request.user).filter(book__space=self.request.space) class MealPlanViewSet(viewsets.ModelViewSet): @@ -223,7 +223,7 @@ class MealPlanViewSet(viewsets.ModelViewSet): queryset = self.queryset.filter( Q(created_by=self.request.user) | Q(shared=self.request.user) - ).filter(space=self.request.user.userpreference.space).distinct().all() + ).filter(space=self.request.space).distinct().all() from_date = self.request.query_params.get('from_date', None) if from_date is not None: @@ -245,7 +245,7 @@ class MealTypeViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsOwner] def get_queryset(self): - queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() + queryset = self.queryset.order_by('order', 'id').filter(created_by=self.request.user).filter(space=self.request.space).all() return queryset @@ -255,7 +255,7 @@ class IngredientViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsUser] def get_queryset(self): - return self.queryset.filter(step__recipe__space=self.request.user.userpreference.space) + return self.queryset.filter(step__recipe__space=self.request.space) class StepViewSet(viewsets.ModelViewSet): @@ -264,7 +264,7 @@ class StepViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsUser] def get_queryset(self): - return self.queryset.filter(recipe__space=self.request.user.userpreference.space) + return self.queryset.filter(recipe__space=self.request.space) class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin): @@ -340,7 +340,7 @@ class ShoppingListRecipeViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsOwner, ] def get_queryset(self): - return self.queryset.filter(shoppinglist__created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() + return self.queryset.filter(shoppinglist__created_by=self.request.user).filter(space=self.request.space).all() class ShoppingListEntryViewSet(viewsets.ModelViewSet): @@ -349,7 +349,7 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsOwner, ] def get_queryset(self): - return self.queryset.filter(shoppinglist__created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() + return self.queryset.filter(shoppinglist__created_by=self.request.user).filter(space=self.request.space).all() class ShoppingListViewSet(viewsets.ModelViewSet): @@ -358,9 +358,7 @@ class ShoppingListViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsOwner | CustomIsShared] def get_queryset(self): - return self.queryset.filter( - Q(created_by=self.request.user) | Q(shared=self.request.user) - ).filter(space=self.request.user.userpreference.space).all() + return self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(space=self.request.space).distinct() def get_serializer_class(self): autosync = self.request.query_params.get('autosync', None) @@ -375,7 +373,7 @@ class ViewLogViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsOwner] def get_queryset(self): - self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() + self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space).all() if self.request.method == 'GET': return self.queryset[:5] else: @@ -388,7 +386,7 @@ class CookLogViewSet(viewsets.ModelViewSet): permission_classes = [CustomIsOwner] def get_queryset(self): - self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.user.userpreference.space).all() + self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space).all() if self.request.method == 'GET': return self.queryset[:5] else: