From f1a1b93ee54b0946f7f2fd3027935dd9265f2d8c Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
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 00000000..7e214d7d
--- /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 00000000..075e957f
--- /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]
-- 
GitLab