From f1a1b93ee54b0946f7f2fd3027935dd9265f2d8c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 23 May 2018 19:52:47 +0200 Subject: [PATCH] See #228: serializer logic --- api/funkwhale_api/common/serializers.py | 74 ++++++++++++++++++++ api/tests/common/test_serializers.py | 89 +++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 api/funkwhale_api/common/serializers.py create mode 100644 api/tests/common/test_serializers.py diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py new file mode 100644 index 000000000..7e214d7db --- /dev/null +++ b/api/funkwhale_api/common/serializers.py @@ -0,0 +1,74 @@ +from rest_framework import serializers + + +class ActionSerializer(serializers.Serializer): + """ + A special serializer that can operate on a list of objects + and apply actions on it. + """ + + action = serializers.CharField(required=True) + objects = serializers.JSONField(required=True) + filters = serializers.DictField(required=False) + actions = None + filterset_class = None + + def __init__(self, *args, **kwargs): + self.queryset = kwargs.pop('queryset') + if self.actions is None: + raise ValueError( + 'You must declare a list of actions on ' + 'the serializer class') + + for action in self.actions: + handler_name = 'handle_{}'.format(action) + assert hasattr(self, handler_name), ( + '{} miss a {} method'.format( + self.__class__.__name__, handler_name) + ) + super().__init__(self, *args, **kwargs) + + def validate_action(self, value): + if value not in self.actions: + raise serializers.ValidationError( + '{} is not a valid action. Pick one of {}.'.format( + value, ', '.join(self.actions) + ) + ) + return value + + def validate_objects(self, value): + qs = None + if value == 'all': + return self.queryset.all().order_by('id') + if type(value) in [list, tuple]: + return self.queryset.filter(pk__in=value).order_by('id') + + raise serializers.ValidationError( + '{} is not a valid value for objects. You must provide either a ' + 'list of identifiers or the string "all".'.format(value)) + + def validate(self, data): + if not self.filterset_class or 'filters' not in data: + # no additional filters to apply, we just skip + return data + + qs_filterset = self.filterset_class( + data['filters'], queryset=data['objects']) + try: + assert qs_filterset.form.is_valid() + except (AssertionError, TypeError): + raise serializers.ValidationError('Invalid filters') + data['objects'] = qs_filterset.qs + return data + + def save(self): + handler_name = 'handle_{}'.format(self.validated_data['action']) + handler = getattr(self, handler_name) + result = handler(self.validated_data['objects']) + payload = { + 'updated': self.validated_data['objects'].count(), + 'action': self.validated_data['action'], + 'result': result, + } + return payload diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py new file mode 100644 index 000000000..075e957f6 --- /dev/null +++ b/api/tests/common/test_serializers.py @@ -0,0 +1,89 @@ +import django_filters + +from funkwhale_api.common import serializers +from funkwhale_api.users import models + + +class TestActionFilterSet(django_filters.FilterSet): + class Meta: + model = models.User + fields = ['is_active'] + + +class TestSerializer(serializers.ActionSerializer): + actions = ['test'] + filterset_class = TestActionFilterSet + + def handle_test(self, objects): + return {'hello': 'world'} + + +def test_action_serializer_validates_action(): + data = {'objects': 'all', 'action': 'nope'} + serializer = TestSerializer(data, queryset=models.User.objects.none()) + + assert serializer.is_valid() is False + assert 'action' in serializer.errors + + +def test_action_serializer_validates_objects(): + data = {'objects': 'nope', 'action': 'test'} + serializer = TestSerializer(data, queryset=models.User.objects.none()) + + assert serializer.is_valid() is False + assert 'objects' in serializer.errors + + +def test_action_serializers_objects_clean_ids(factories): + user1 = factories['users.User']() + user2 = factories['users.User']() + + data = {'objects': [user1.pk], 'action': 'test'} + serializer = TestSerializer(data, queryset=models.User.objects.all()) + + assert serializer.is_valid() is True + assert list(serializer.validated_data['objects']) == [user1] + + +def test_action_serializers_objects_clean_all(factories): + user1 = factories['users.User']() + user2 = factories['users.User']() + + data = {'objects': 'all', 'action': 'test'} + serializer = TestSerializer(data, queryset=models.User.objects.all()) + + assert serializer.is_valid() is True + assert list(serializer.validated_data['objects']) == [user1, user2] + + +def test_action_serializers_save(factories, mocker): + handler = mocker.spy(TestSerializer, 'handle_test') + user1 = factories['users.User']() + user2 = factories['users.User']() + + data = {'objects': 'all', 'action': 'test'} + serializer = TestSerializer(data, queryset=models.User.objects.all()) + + assert serializer.is_valid() is True + result = serializer.save() + assert result == { + 'updated': 2, + 'action': 'test', + 'result': {'hello': 'world'}, + } + handler.assert_called_once() + + +def test_action_serializers_filterset(factories): + user1 = factories['users.User'](is_active=False) + user2 = factories['users.User'](is_active=True) + + data = { + 'objects': 'all', + 'action': 'test', + 'filters': {'is_active': True}, + } + serializer = TestSerializer(data, queryset=models.User.objects.all()) + + assert serializer.is_valid() is True + assert list(serializer.validated_data['objects']) == [user2]