serializers.py 10.5 KB
Newer Older
1
import collections
2
3
4
import io
import PIL
import os
5

Eliot Berriot's avatar
Eliot Berriot committed
6
7
from rest_framework import serializers

8
from django.core.exceptions import ObjectDoesNotExist
9
from django.core.files.uploadedfile import SimpleUploadedFile
10
11
12
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _

13
from . import models
14
from . import utils
15

16
17
18
19
20
21
22
23
24
25
26

class RelatedField(serializers.RelatedField):
    default_error_messages = {
        "does_not_exist": _("Object with {related_field_name}={value} does not exist."),
        "invalid": _("Invalid value."),
    }

    def __init__(self, related_field_name, serializer, **kwargs):
        self.related_field_name = related_field_name
        self.serializer = serializer
        self.filters = kwargs.pop("filters", None)
Eliot Berriot's avatar
Eliot Berriot committed
27
        self.queryset_filter = kwargs.pop("queryset_filter", None)
28
29
30
31
        try:
            kwargs["queryset"] = kwargs.pop("queryset")
        except KeyError:
            kwargs["queryset"] = self.serializer.Meta.model.objects.all()
32
33
34
35
36
37
38
39
        super().__init__(**kwargs)

    def get_filters(self, data):
        filters = {self.related_field_name: data}
        if self.filters:
            filters.update(self.filters(self.context))
        return filters

Eliot Berriot's avatar
Eliot Berriot committed
40
41
42
43
44
    def filter_queryset(self, queryset):
        if self.queryset_filter:
            queryset = self.queryset_filter(queryset, self.context)
        return queryset

45
46
47
48
    def to_internal_value(self, data):
        try:
            queryset = self.get_queryset()
            filters = self.get_filters(data)
Eliot Berriot's avatar
Eliot Berriot committed
49
            queryset = self.filter_queryset(queryset)
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
            return queryset.get(**filters)
        except ObjectDoesNotExist:
            self.fail(
                "does_not_exist",
                related_field_name=self.related_field_name,
                value=smart_text(data),
            )
        except (TypeError, ValueError):
            self.fail("invalid")

    def to_representation(self, obj):
        return self.serializer.to_representation(obj)

    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            # Ensure that field.choices returns something sensible
            # even when accessed with a read-only field.
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        return collections.OrderedDict(
            [
                (
                    self.to_representation(item)[self.related_field_name],
                    self.display_value(item),
                )
                for item in queryset
80
                if self.serializer
81
82
83
            ]
        )

Eliot Berriot's avatar
Eliot Berriot committed
84

85
class Action(object):
86
    def __init__(self, name, allow_all=False, qs_filter=None):
87
88
        self.name = name
        self.allow_all = allow_all
89
        self.qs_filter = qs_filter
90
91
92
93
94

    def __repr__(self):
        return "<Action {}>".format(self.name)


Eliot Berriot's avatar
Eliot Berriot committed
95
96
97
98
99
100
101
102
103
104
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
105
    pk_field = "pk"
Eliot Berriot's avatar
Eliot Berriot committed
106
107

    def __init__(self, *args, **kwargs):
108
        self.actions_by_name = {a.name: a for a in self.actions}
Eliot Berriot's avatar
Eliot Berriot committed
109
        self.queryset = kwargs.pop("queryset")
Eliot Berriot's avatar
Eliot Berriot committed
110
111
        if self.actions is None:
            raise ValueError(
Eliot Berriot's avatar
Eliot Berriot committed
112
113
                "You must declare a list of actions on " "the serializer class"
            )
Eliot Berriot's avatar
Eliot Berriot committed
114

115
        for action in self.actions_by_name.keys():
Eliot Berriot's avatar
Eliot Berriot committed
116
117
118
            handler_name = "handle_{}".format(action)
            assert hasattr(self, handler_name), "{} miss a {} method".format(
                self.__class__.__name__, handler_name
Eliot Berriot's avatar
Eliot Berriot committed
119
120
121
122
            )
        super().__init__(self, *args, **kwargs)

    def validate_action(self, value):
123
124
125
        try:
            return self.actions_by_name[value]
        except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
126
            raise serializers.ValidationError(
Eliot Berriot's avatar
Eliot Berriot committed
127
                "{} is not a valid action. Pick one of {}.".format(
128
                    value, ", ".join(self.actions_by_name.keys())
Eliot Berriot's avatar
Eliot Berriot committed
129
130
131
132
                )
            )

    def validate_objects(self, value):
Eliot Berriot's avatar
Eliot Berriot committed
133
134
        if value == "all":
            return self.queryset.all().order_by("id")
Eliot Berriot's avatar
Eliot Berriot committed
135
        if type(value) in [list, tuple]:
136
137
            return self.queryset.filter(
                **{"{}__in".format(self.pk_field): value}
138
            ).order_by(self.pk_field)
Eliot Berriot's avatar
Eliot Berriot committed
139
140

        raise serializers.ValidationError(
Eliot Berriot's avatar
Eliot Berriot committed
141
142
143
            "{} is not a valid value for objects. You must provide either a "
            'list of identifiers or the string "all".'.format(value)
        )
Eliot Berriot's avatar
Eliot Berriot committed
144
145

    def validate(self, data):
146
147
        allow_all = data["action"].allow_all
        if not allow_all and self.initial_data["objects"] == "all":
148
            raise serializers.ValidationError(
149
                "You cannot apply this action on all objects"
Eliot Berriot's avatar
Eliot Berriot committed
150
            )
151
152
153
        final_filters = data.get("filters", {}) or {}
        if self.filterset_class and final_filters:
            qs_filterset = self.filterset_class(final_filters, queryset=data["objects"])
154
155
156
            try:
                assert qs_filterset.form.is_valid()
            except (AssertionError, TypeError):
Eliot Berriot's avatar
Eliot Berriot committed
157
158
                raise serializers.ValidationError("Invalid filters")
            data["objects"] = qs_filterset.qs
Eliot Berriot's avatar
Eliot Berriot committed
159

160
161
162
        if data["action"].qs_filter:
            data["objects"] = data["action"].qs_filter(data["objects"])

Eliot Berriot's avatar
Eliot Berriot committed
163
164
165
        data["count"] = data["objects"].count()
        if data["count"] < 1:
            raise serializers.ValidationError("No object matching your request")
Eliot Berriot's avatar
Eliot Berriot committed
166
167
168
        return data

    def save(self):
169
        handler_name = "handle_{}".format(self.validated_data["action"].name)
Eliot Berriot's avatar
Eliot Berriot committed
170
        handler = getattr(self, handler_name)
Eliot Berriot's avatar
Eliot Berriot committed
171
        result = handler(self.validated_data["objects"])
Eliot Berriot's avatar
Eliot Berriot committed
172
        payload = {
Eliot Berriot's avatar
Eliot Berriot committed
173
            "updated": self.validated_data["count"],
174
            "action": self.validated_data["action"].name,
Eliot Berriot's avatar
Eliot Berriot committed
175
            "result": result,
Eliot Berriot's avatar
Eliot Berriot committed
176
177
        }
        return payload
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208


def track_fields_for_update(*fields):
    """
    Apply this decorator to serializer to call function when specific values
    are updated on an object:

    .. code-block:: python

        @track_fields_for_update('privacy_level')
        class LibrarySerializer(serializers.ModelSerializer):
            def on_updated_privacy_level(self, obj, old_value, new_value):
                print('Do someting')
    """

    def decorator(serializer_class):
        original_update = serializer_class.update

        def new_update(self, obj, validated_data):
            tracked_fields_before = {f: getattr(obj, f) for f in fields}
            obj = original_update(self, obj, validated_data)
            tracked_fields_after = {f: getattr(obj, f) for f in fields}

            if tracked_fields_before != tracked_fields_after:
                self.on_updated_fields(obj, tracked_fields_before, tracked_fields_after)
            return obj

        serializer_class.update = new_update
        return serializer_class

    return decorator
209
210
211
212
213
214
215
216
217
218
219
220
221
222


class StripExifImageField(serializers.ImageField):
    def to_internal_value(self, data):
        file_obj = super().to_internal_value(data)

        image = PIL.Image.open(file_obj)
        data = list(image.getdata())
        image_without_exif = PIL.Image.new(image.mode, image.size)
        image_without_exif.putdata(data)

        with io.BytesIO() as output:
            image_without_exif.save(
                output,
223
                format=PIL.Image.EXTENSION[os.path.splitext(file_obj.name)[-1].lower()],
224
225
226
227
228
229
230
                quality=100,
            )
            content = output.getvalue()

        return SimpleUploadedFile(
            file_obj.name, content, content_type=file_obj.content_type
        )
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284


from funkwhale_api.federation import serializers as federation_serializers  # noqa

TARGET_ID_TYPE_MAPPING = {
    "music.Track": ("id", "track"),
    "music.Artist": ("id", "artist"),
    "music.Album": ("id", "album"),
}


class APIMutationSerializer(serializers.ModelSerializer):
    created_by = federation_serializers.APIActorSerializer(read_only=True)
    target = serializers.SerializerMethodField()

    class Meta:
        model = models.Mutation
        fields = [
            "fid",
            "uuid",
            "type",
            "creation_date",
            "applied_date",
            "is_approved",
            "is_applied",
            "created_by",
            "approved_by",
            "summary",
            "payload",
            "previous_state",
            "target",
        ]
        read_only_fields = [
            "uuid",
            "creation_date",
            "fid",
            "is_applied",
            "created_by",
            "approved_by",
            "previous_state",
        ]

    def get_target(self, obj):
        target = obj.target
        if not target:
            return

        id_field, type = TARGET_ID_TYPE_MAPPING[target._meta.label]
        return {"type": type, "id": getattr(target, id_field), "repr": str(target)}

    def validate_type(self, value):
        if value not in self.context["registry"]:
            raise serializers.ValidationError("Invalid mutation type {}".format(value))
        return value
Eliot Berriot's avatar
Eliot Berriot committed
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299


class AttachmentSerializer(serializers.Serializer):
    uuid = serializers.UUIDField(read_only=True)
    size = serializers.IntegerField(read_only=True)
    mimetype = serializers.CharField(read_only=True)
    creation_date = serializers.DateTimeField(read_only=True)
    file = StripExifImageField(write_only=True)
    urls = serializers.SerializerMethodField()

    def get_urls(self, o):
        urls = {}
        urls["source"] = o.url
        urls["original"] = o.download_url_original
        urls["medium_square_crop"] = o.download_url_medium_square_crop
300
        urls["large_square_crop"] = o.download_url_large_square_crop
Eliot Berriot's avatar
Eliot Berriot committed
301
302
303
304
305
306
        return urls

    def create(self, validated_data):
        return models.Attachment.objects.create(
            file=validated_data["file"], actor=validated_data["actor"]
        )
307
308
309


class ContentSerializer(serializers.Serializer):
310
311
312
    text = serializers.CharField(
        max_length=models.CONTENT_TEXT_MAX_LENGTH, allow_null=True
    )
313
314
315
316
317
    content_type = serializers.ChoiceField(choices=models.CONTENT_TEXT_SUPPORTED_TYPES,)
    html = serializers.SerializerMethodField()

    def get_html(self, o):
        return utils.render_html(o.text, o.content_type)
Eliot Berriot's avatar
Eliot Berriot committed
318
319
320
321
322
323
324
325
326
327
328
329
330


class NullToEmptDict(object):
    def get_attribute(self, o):
        attr = super().get_attribute(o)
        if attr is None:
            return {}
        return attr

    def to_representation(self, v):
        if not v:
            return v
        return super().to_representation(v)