serializers.py 25.5 KB
Newer Older
1
from collections import OrderedDict, Mapping, Iterable
2
from typing import Any
3 4
from urllib import parse

5
from django.conf import settings
6
from django.core.exceptions import ImproperlyConfigured
Jean-Baptiste's avatar
Jean-Baptiste committed
7
from django.core.exceptions import ValidationError as DjangoValidationError
8
from django.core.urlresolvers import get_resolver, resolve, get_script_prefix, Resolver404
9
from django.db.models import QuerySet
10
from django.utils.datastructures import MultiValueDictKeyError
11
from django.utils.encoding import uri_to_iri
12
from rest_framework.exceptions import ValidationError
13
from rest_framework.fields import SkipField, empty, ReadOnlyField
14
from rest_framework.fields import get_error_detail, set_value
15
from rest_framework.relations import HyperlinkedRelatedField, ManyRelatedField, MANY_RELATION_KWARGS, Hyperlink
16
from rest_framework.serializers import HyperlinkedModelSerializer, ListSerializer, ModelSerializer
17
from rest_framework.settings import api_settings
Jean-Baptiste's avatar
Jean-Baptiste committed
18
from rest_framework.utils import model_meta
19
from rest_framework.utils.field_mapping import get_nested_relation_kwargs
Jean-Baptiste's avatar
Jean-Baptiste committed
20 21
from rest_framework.utils.serializer_helpers import ReturnDict

22
from djangoldp.fields import LDPUrlField, IdURLField
Thibaud's avatar
Thibaud committed
23
from djangoldp.models import Model
24
from djangoldp.permissions import LDPPermissions
Sylvain Le Bon's avatar
Sylvain Le Bon committed
25

Thibaud's avatar
Thibaud committed
26

27
class LDListMixin:
Jean-Baptiste's avatar
Jean-Baptiste committed
28 29
    child_attr = 'child'

30
    def to_internal_value(self, data):
31 32
        try:
            data = data['ldp:contains']
33
        except (TypeError, KeyError):
34
            pass
35
        if isinstance(data, dict):
36
            data = [data]
Sylvain Le Bon's avatar
Sylvain Le Bon committed
37
        if isinstance(data, str) and data.startswith("http"):
38
            data = [{'@id': data}]
Jean-Baptiste's avatar
Jean-Baptiste committed
39 40

        return [getattr(self, self.child_attr).to_internal_value(item) for item in data]
Jean-Baptiste's avatar
Jean-Baptiste committed
41

42
    def to_representation(self, value):
43 44 45 46 47
        '''
        Permission on container :
         - Can Add if add permission on contained object's type
         - Can view the container is view permission on container model : container obj are filtered by view permission
        '''
48
        try:
49
            child_model = getattr(self, self.child_attr).Meta.model
50
        except AttributeError:
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
            child_model = value.model
        parent_model = None
        if isinstance(value, QuerySet):
            value = list(value)

        if not isinstance(value, Iterable):
            filtered_values = value
            container_permissions = Model.get_permissions(child_model, self.context['request'].user, ['view', 'add'])
        else:
            try:
                parent_model = Model.resolve_parent(self.context['request'].path)
            except:
                parent_model = child_model

            filtered_values = list(
                filter(lambda v: Model.get_permission_classes(v, [LDPPermissions])[0]().has_object_permission(
                    self.context['request'], self.context['view'], v), value))
            container_permissions = Model.get_permissions(child_model, self.context['request'].user, ['add'])
            container_permissions.extend(
                Model.get_permissions(parent_model, self.context['request'].user,
                                      ['view']))
72 73
        return {'@id': self.id,
                '@type': 'ldp:Container',
74 75
                'ldp:contains': super().to_representation(filtered_values),
                'permissions': container_permissions
76
                }
Jean-Baptiste's avatar
Jean-Baptiste committed
77

78
    def get_attribute(self, instance):
79 80 81
        parent_id_field = self.parent.fields[self.parent.url_field_name]
        context = self.parent.context
        parent_id = parent_id_field.get_url(instance, parent_id_field.view_name, context['request'], context['format'])
Jean-Baptiste's avatar
Jean-Baptiste committed
82
        self.id = parent_id + self.field_name + "/"
83
        return super().get_attribute(instance)
84

85 86
    def get_value(self, dictionary):
        try:
87 88
            object_list = dictionary['@graph']

Thibaud's avatar
Thibaud committed
89 90 91 92 93 94
            if self.parent.instance is None:
                obj = next(filter(
                    lambda o: not hasattr(o, self.parent.url_field_name) or "./" in o[self.parent.url_field_name],
                    object_list))
            else:
                container_id = Model.container_id(self.parent.instance)
95
                obj = next(filter(lambda o: container_id.lstrip('/') in o[self.parent.url_field_name], object_list))
96 97
            list = super().get_value(obj)
            try:
Thibaud's avatar
Thibaud committed
98 99
                list = next(
                    filter(lambda o: list[self.parent.url_field_name] == o[self.parent.url_field_name], object_list))
100
            except (KeyError, TypeError, StopIteration):
101 102
                pass

103
            try:
104
                list = list['ldp:contains']
105
            except (KeyError, TypeError):
106 107
                pass

Thibaud's avatar
Thibaud committed
108 109 110
            if list is empty:
                return []

111 112 113
            if isinstance(list, dict):
                list = [list]

114
            ret = []
115
            for item in list:
116
                full_item = None
117
                try:
Thibaud's avatar
Thibaud committed
118 119 120
                    full_item = next(filter(
                        lambda o: self.parent.url_field_name in o and item[self.parent.url_field_name] == o[
                            self.parent.url_field_name], object_list))
121 122
                except StopIteration:
                    pass
123
                if full_item is None:
124 125
                    ret.append(item)
                else:
126
                    ret.append(full_item)
127 128

            return ret
129
        except KeyError:
130 131 132 133 134 135 136 137
            obj = super().get_value(dictionary)
            if isinstance(obj, dict) and self.parent.url_field_name in obj:
                resource_id = obj[self.parent.url_field_name]
                if isinstance(resource_id, str) and resource_id.startswith("_:"):
                    object_list = self.root.initial_data['@graph']
                    obj = [next(filter(lambda o: resource_id in o[self.parent.url_field_name], object_list))]

            return obj
138

Jean-Baptiste's avatar
Jean-Baptiste committed
139

140
class ContainerSerializer(LDListMixin, ListSerializer):
Jean-Baptiste's avatar
Jean-Baptiste committed
141 142
    id = ''

Sylvain Le Bon's avatar
Sylvain Le Bon committed
143 144 145
    @property
    def data(self):
        return ReturnDict(super(ListSerializer, self).data, serializer=self)
146 147 148

    def create(self, validated_data):
        return super().create(validated_data)
Jean-Baptiste's avatar
Jean-Baptiste committed
149

150 151
    def to_internal_value(self, data):
        try:
Thibaud's avatar
Thibaud committed
152
            return super().to_internal_value(data[self.parent.url_field_name])
153
        except (KeyError, TypeError):
154 155
            return super().to_internal_value(data)

Sylvain Le Bon's avatar
Sylvain Le Bon committed
156

157
class ManyJsonLdRelatedField(LDListMixin, ManyRelatedField):
Jean-Baptiste's avatar
Jean-Baptiste committed
158 159 160
    child_attr = 'child_relation'
    url_field_name = "@id"

161

162
class JsonLdField(HyperlinkedRelatedField):
163 164
    def __init__(self, view_name=None, **kwargs):
        super().__init__(view_name, **kwargs)
165
        self.get_lookup_args()
Jean-Baptiste's avatar
Jean-Baptiste committed
166

167
    def get_lookup_args(self):
168
        try:
169 170 171
            lookup_field = get_resolver().reverse_dict[self.view_name][0][0][1][0]
            self.lookup_field = lookup_field
            self.lookup_url_kwarg = lookup_field
172 173
        except MultiValueDictKeyError:
            pass
174

175 176 177 178 179 180
    def to_internal_value(self, data):
        return super().to_internal_value(data)

    def get_value(self, dictionary):
        return super().get_value(dictionary)

Jean-Baptiste's avatar
Jean-Baptiste committed
181

182
class JsonLdRelatedField(JsonLdField):
183
    def to_representation(self, value):
184 185 186 187
        try:
            return {'@id': super().to_representation(value)}
        except ImproperlyConfigured:
            return value.pk
Jean-Baptiste's avatar
Jean-Baptiste committed
188

189 190
    def to_internal_value(self, data):
        try:
Thibaud's avatar
Thibaud committed
191
            return super().to_internal_value(data[self.parent.url_field_name])
192
        except (KeyError, TypeError):
193
            return super().to_internal_value(data)
Jean-Baptiste's avatar
Jean-Baptiste committed
194

195 196 197 198 199 200
    @classmethod
    def many_init(cls, *args, **kwargs):
        list_kwargs = {'child_relation': cls(*args, **kwargs)}
        for key in kwargs:
            if key in MANY_RELATION_KWARGS:
                list_kwargs[key] = kwargs[key]
201
        return ManyJsonLdRelatedField(**list_kwargs)
202

Jean-Baptiste's avatar
Jean-Baptiste committed
203

204
class JsonLdIdentityField(JsonLdField):
205 206 207
    def __init__(self, view_name=None, **kwargs):
        kwargs['read_only'] = True
        kwargs['source'] = '*'
208
        super().__init__(view_name, **kwargs)
Jean-Baptiste's avatar
Jean-Baptiste committed
209

210 211 212
    def use_pk_only_optimization(self):
        return False

213 214
    def to_internal_value(self, data):
        try:
Thibaud's avatar
Thibaud committed
215
            return super().to_internal_value(data[self.parent.url_field_name])
216
        except KeyError:
217 218
            return super().to_internal_value(data)

219 220 221
    def get_value(self, dictionary):
        return super().get_value(dictionary)

222 223 224 225 226 227
    def to_representation(self, value: Any) -> Any:
        try:
            return Hyperlink(value.webid(), value)
        except AttributeError:
            return super().to_representation(value)

Jean-Baptiste's avatar
Jean-Baptiste committed
228

229 230 231
class LDPSerializer(HyperlinkedModelSerializer):
    url_field_name = "@id"
    serializer_related_field = JsonLdRelatedField
232
    serializer_url_field = JsonLdIdentityField
Thibaud's avatar
Thibaud committed
233
    ModelSerializer.serializer_field_mapping[LDPUrlField] = IdURLField
Jean-Baptiste's avatar
Jean-Baptiste committed
234

235
    def get_default_field_names(self, declared_fields, model_info):
236 237
        try:
            fields = list(self.Meta.model._meta.serializer_fields)
238
        except AttributeError:
239
            fields = super().get_default_field_names(declared_fields, model_info)
240 241 242 243 244 245 246
        if 'request' in self._context and not self._context['request']._request.method == 'GET':
            try:
                fields.remove(self.Meta.model._meta.auto_author)
            except ValueError:
                pass
            except AttributeError:
                pass
Rob's avatar
Rob committed
247
        return fields + list(getattr(self.Meta, 'extra_fields', []))
Jean-Baptiste's avatar
Jean-Baptiste committed
248

249 250
    def to_representation(self, obj):
        data = super().to_representation(obj)
251 252
        slug_field = Model.slug_field(obj)
        for field in data:
253
            if isinstance(data[field], dict) and '@id' in data[field]:
254
                data[field]['@id'] = data[field]['@id'].format(Model.container_id(obj), str(getattr(obj, slug_field)))
255 256 257 258 259 260
        rdf_type = Model.get_meta(obj, 'rdf_type', None)
        rdf_context = Model.get_meta(obj, 'rdf_context', None)
        if rdf_type is not None:
            data['@type'] = rdf_type
        if rdf_context is not None:
            data['@context'] = rdf_context
261 262
        data['permissions'] = Model.get_permissions(obj, self.context['request'].user,
                                                    ['view', 'change', 'control', 'delete'])
263

264
        return data
Jean-Baptiste's avatar
Jean-Baptiste committed
265

266 267 268
    def build_field(self, field_name, info, model_class, nested_depth):
        return super().build_field(field_name, info, model_class, nested_depth)

269 270 271
    def build_property_field(self, field_name, model_class):
        class JSonLDPropertyField(ReadOnlyField):
            def to_representation(self, instance):
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
                from djangoldp.views import LDPViewSet
                try:
                    model_class = instance.model
                except :
                    model_class = instance.__class__
                serializer_generator = LDPViewSet(model=model_class,
                                                  lookup_field=Model.get_meta(model_class, 'lookup_field', 'pk'),
                                                  permission_classes=Model.get_meta(model_class,
                                                                                    'permission_classes',
                                                                                    [LDPPermissions]),
                                                  fields=Model.get_meta(model_class, 'serializer_fields', []),
                                                  nested_fields=Model.get_meta(model_class, 'nested_fields', []))
                parent_depth = max(getattr(self.parent.Meta, "depth", 0) - 1, 0)
                serializer_generator.depth = parent_depth
                serializer = serializer_generator.build_read_serializer()(context=self.parent.context)
                if parent_depth is 0:
                    serializer.Meta.fields = ["@id"]

290 291
                if isinstance(instance, QuerySet):
                    data = list(instance)
292

293
                    return {'@id': '{}{}{}/'.format(settings.SITE_URL, '{}{}/', self.source),
294 295 296 297 298 299 300
                            '@type': 'ldp:Container',
                            'ldp:contains': [serializer.to_representation(item) if item is not None else None for item
                                             in data],
                            'permissions': Model.get_permissions(self.parent.Meta.model, self.context['request'].user,
                                                                 ['view', 'add'])
                            }
                else:
301
                    return serializer.to_representation(instance)
302 303 304 305 306 307

        field_class = JSonLDPropertyField
        field_kwargs = {}

        return field_class, field_kwargs

308 309 310 311 312 313 314 315 316 317 318
    def build_standard_field(self, field_name, model_field):
        class JSonLDStandardField:
            parent_view_name = None

            def __init__(self, **kwargs):
                self.parent_view_name = kwargs.pop('parent_view_name')
                super().__init__(**kwargs)

            def get_value(self, dictionary):
                try:
                    object_list = dictionary["@graph"]
Thibaud's avatar
Thibaud committed
319 320
                    if self.parent.instance is None:
                        obj = next(filter(
321
                            lambda o: not self.parent.url_field_name in o or "./" in o[self.parent.url_field_name],
Thibaud's avatar
Thibaud committed
322
                            object_list))
323
                        value = super().get_value(obj)
Thibaud's avatar
Thibaud committed
324 325
                    else:
                        resource_id = Model.resource_id(self.parent.instance)
Jean-Baptiste's avatar
Jean-Baptiste committed
326 327 328
                        obj = next(
                            filter(lambda o: resource_id.lstrip('/') in o[self.parent.url_field_name], object_list))
                        value = super().get_value(obj)
329
                except KeyError:
330 331 332 333 334 335 336 337 338 339 340 341 342 343
                    value = super().get_value(dictionary)

                return self.manage_empty(value)

            def manage_empty(self, value):
                if value == '' and self.allow_null:
                    # If the field is blank, and null is a valid value then
                    # determine if we should use null instead.
                    return '' if getattr(self, 'allow_blank', False) else None
                elif value == '' and not self.required:
                    # If the field is blank, and emptiness is valid then
                    # determine if we should use emptiness instead.
                    return '' if getattr(self, 'allow_blank', False) else empty
                return value
344 345 346

        field_class, field_kwargs = super().build_standard_field(field_name, model_field)
        field_kwargs['parent_view_name'] = '{}-list'.format(model_field.model._meta.object_name.lower())
347
        return type(field_class.__name__ + 'Valued', (JSonLDStandardField, field_class), {}), field_kwargs
348

349
    def build_nested_field(self, field_name, relation_info, nested_depth):
350

351
        class NestedLDPSerializer(self.__class__):
352

353 354 355
            class Meta:
                model = relation_info.related_model
                depth = nested_depth - 1
356
                try:
357 358
                    fields = ['@id'] + list(model._meta.serializer_fields)
                except AttributeError:
359 360
                    fields = '__all__'

Jean-Baptiste's avatar
Jean-Baptiste committed
361
            def to_internal_value(self, data):
362 363
                if data is '':
                    return ''
364
                if self.url_field_name in data:
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
                    if not isinstance(data, Mapping):
                        message = self.error_messages['invalid'].format(
                            datatype=type(data).__name__
                        )
                        raise ValidationError({
                            api_settings.NON_FIELD_ERRORS_KEY: [message]
                        }, code='invalid')

                    ret = OrderedDict()
                    errors = OrderedDict()
                    fields = list(filter(lambda x: x.field_name in data, self._writable_fields))

                    for field in fields:
                        validate_method = getattr(self, 'validate_' + field.field_name, None)
                        primitive_value = field.get_value(data)
                        try:
                            validated_value = field.run_validation(primitive_value)
                            if validate_method is not None:
                                validated_value = validate_method(validated_value)
                        except ValidationError as exc:
                            errors[field.field_name] = exc.detail
                        except DjangoValidationError as exc:
                            errors[field.field_name] = get_error_detail(exc)
                        except SkipField:
                            pass
                        else:
                            set_value(ret, field.source_attrs, validated_value)

                    if errors:
                        raise ValidationError(errors)

396
                    uri = data[self.url_field_name]
397 398 399 400 401 402 403 404
                    http_prefix = uri.startswith(('http:', 'https:'))

                    if http_prefix:
                        uri = parse.urlparse(uri).path
                        prefix = get_script_prefix()
                        if uri.startswith(prefix):
                            uri = '/' + uri[len(prefix):]

405 406
                    try:
                        match = resolve(uri_to_iri(uri))
407 408
                        slug_field = Model.slug_field(self.__class__.Meta.model)
                        ret[slug_field] = match.kwargs[slug_field]
409 410
                    except Resolver404:
                        pass
411

412 413 414
                    return ret
                else:
                    return super().to_internal_value(data)
Jean-Baptiste's avatar
Jean-Baptiste committed
415 416 417

        kwargs = get_nested_relation_kwargs(relation_info)
        kwargs['read_only'] = False
418
        kwargs['required'] = False
Jean-Baptiste's avatar
Jean-Baptiste committed
419 420
        return NestedLDPSerializer, kwargs

Sylvain Le Bon's avatar
Sylvain Le Bon committed
421 422
    @classmethod
    def many_init(cls, *args, **kwargs):
423
        kwargs['child'] = cls(**kwargs)
Sylvain Le Bon's avatar
Sylvain Le Bon committed
424
        return ContainerSerializer(*args, **kwargs)
Jean-Baptiste's avatar
Jean-Baptiste committed
425

426
    def get_value(self, dictionary):
427 428
        try:
            object_list = dictionary["@graph"]
429 430 431 432 433 434
            if self.parent.instance is None:
                obj = next(filter(
                    lambda o: not hasattr(o, self.parent.url_field_name) or "./" in o[self.url_field_name],
                    object_list))
            else:
                container_id = Model.container_id(self.parent.instance)
435
                obj = next(filter(lambda o: container_id.lstrip('/') in o[self.url_field_name], object_list))
436 437
            item = super().get_value(obj)
            full_item = None
438 439
            if item is empty:
                return empty
440
            try:
Jean-Baptiste's avatar
Jean-Baptiste committed
441 442 443
                full_item = next(
                    filter(lambda o: self.url_field_name in o and (item[self.url_field_name] == o[self.url_field_name]),
                           object_list))
444 445 446 447 448 449 450 451 452
            except StopIteration:
                pass
            if full_item is None:
                return item
            else:
                return full_item

        except KeyError:
            return super().get_value(dictionary)
453

Jean-Baptiste's avatar
Jean-Baptiste committed
454
    def create(self, validated_data):
Jean-Baptiste's avatar
Jean-Baptiste committed
455 456 457 458 459 460 461
        instance = self.internal_create(validated_data, model=self.Meta.model)

        self.attach_related_object(instance, validated_data)

        return instance

    def attach_related_object(self, instance, validated_data):
462
        model_class = self.Meta.model
Jean-Baptiste's avatar
Jean-Baptiste committed
463

464
        info = model_meta.get_field_info(model_class)
Jean-Baptiste's avatar
Jean-Baptiste committed
465 466
        many_to_many = {}
        for field_name, relation_info in info.relations.items():
467
            if relation_info.to_many and relation_info.reverse and not field_name is None:
Jean-Baptiste's avatar
Jean-Baptiste committed
468 469 470 471
                rel = getattr(instance._meta.model, field_name).rel
                if rel.name in validated_data:
                    related = validated_data[rel.name]
                    getattr(instance, field_name).add(related)
472 473

    def internal_create(self, validated_data, model):
474

Jean-Baptiste's avatar
Jean-Baptiste committed
475
        nested_fk_fields_name = list(filter(lambda key: isinstance(validated_data[key], dict), validated_data))
476 477 478 479 480 481 482 483
        for field_name in nested_fk_fields_name:
            field_dict = validated_data[field_name]
            field_model = getattr(model, field_name).field.rel.model
            slug_field = Model.slug_field(field_model)
            if slug_field in field_dict:
                kwargs = {slug_field: field_dict[slug_field]}
                sub_inst = field_model.objects.get(**kwargs)
            else:
Jean-Baptiste's avatar
Jean-Baptiste committed
484
                sub_inst = self.internal_create(field_dict, field_model)
485
            validated_data[field_name] = sub_inst
Jean-Baptiste's avatar
Jean-Baptiste committed
486

487
        nested_fields = []
Jean-Baptiste's avatar
Jean-Baptiste committed
488 489
        nested_list_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data))
        for field_name in nested_list_fields_name:
490
            nested_fields.append((field_name, validated_data.pop(field_name)))
491 492 493 494 495 496 497

        info = model_meta.get_field_info(model)
        many_to_many = []
        for field_name, relation_info in info.relations.items():
            if relation_info.to_many and relation_info.reverse and (
                    field_name in validated_data) and not field_name is None:
                many_to_many.append((field_name, validated_data.pop(field_name)))
498
        validated_data = self.remove_empty_value(validated_data)
499
        instance = model.objects.create(**validated_data)
500

501 502 503
        for field_name, value in many_to_many:
            validated_data[field_name] = value

504
        self.save_or_update_nested_list(instance, nested_fields)
Jean-Baptiste's avatar
Jean-Baptiste committed
505

506
        return instance
507

508 509 510 511 512 513
    def remove_empty_value(self, validated_data):
        for attr, value in validated_data.items():
            if value is '':
                validated_data[attr] = None
        return validated_data

514 515 516 517 518 519 520
    def update(self, instance, validated_data):
        nested_fields = []
        nested_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data))
        for field_name in nested_fields_name:
            nested_fields.append((field_name, validated_data.pop(field_name)))

        for attr, value in validated_data.items():
521
            if isinstance(value, dict):
522
                value = self.update_dict_value(attr, instance, value)
523 524 525 526
            if value is '' and not isinstance(getattr(instance, attr), str):
                setattr(instance, attr, None)
            else:
                setattr(instance, attr, value)
527

528 529
        instance.save()

530
        self.save_or_update_nested_list(instance, nested_fields)
531 532 533

        return instance

534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
    def update_dict_value(self, attr, instance, value):
        info = model_meta.get_field_info(instance)
        slug_field = Model.slug_field(instance)
        relation_info = info.relations.get(attr)
        if slug_field in value:
            value = self.update_dict_value_when_id_is_provided(attr, instance, relation_info, slug_field, value)
        else:
            value = self.update_dict_value_without_slug_field(attr, instance, relation_info, value)
        return value

    def update_dict_value_without_slug_field(self, attr, instance, relation_info, value):
        if relation_info.to_many:
            value = self.internal_create(validated_data=value, model=relation_info.related_model)
        else:
            value[instance._meta.fields_map[attr].remote_field.name] = instance
            oldObj = getattr(instance, attr, None)
            if oldObj is None:
                value = self.internal_create(validated_data=value, model=relation_info.related_model)
            else:
                value = self.update(instance=oldObj, validated_data=value)
        return value

    def update_dict_value_when_id_is_provided(self, attr, instance, relation_info, slug_field, value):
        kwargs = {slug_field: value[slug_field]}
        if relation_info.to_many:
            manager = getattr(instance, attr)
            oldObj = manager._meta.model.objects.get(**kwargs)
        else:
            oldObj = getattr(instance, attr)

        value = self.update(instance=oldObj, validated_data=value)
        return value

567
    def save_or_update_nested_list(self, instance, nested_fields):
568
        for (field_name, data) in nested_fields:
569
            manager = getattr(instance, field_name)
570
            slug_field = Model.slug_field(manager.model)
Jean-Baptiste's avatar
Jean-Baptiste committed
571 572 573
            try:
                item_pk_to_keep = list(map(lambda e: e[slug_field], filter(lambda x: slug_field in x, data)))
            except TypeError:
Jean-Baptiste's avatar
Jean-Baptiste committed
574 575
                item_pk_to_keep = list(
                    map(lambda e: getattr(e, slug_field), filter(lambda x: hasattr(x, slug_field), data)))
576 577

            for item in list(manager.all()):
578
                if not str(getattr(item, slug_field)) in item_pk_to_keep:
579 580 581 582 583
                    if getattr(manager, 'through', None) is None:
                        item.delete()
                    else:
                        manager.remove(item)

584
            for item in data:
Jean-Baptiste's avatar
Jean-Baptiste committed
585 586 587 588
                if not isinstance(item, dict):
                    item.save()
                    saved_item = item
                elif slug_field in item:
589
                    kwargs = {slug_field: item[slug_field]}
590 591 592 593 594
                    try:
                        old_obj = manager.model.objects.get(**kwargs)
                        saved_item = self.update(instance=old_obj, validated_data=item)
                    except manager.model.DoesNotExist:
                        saved_item = self.internal_create(validated_data=item, model=manager.model)
595
                else:
Jean-Baptiste's avatar
Jean-Baptiste committed
596 597
                    rel = getattr(instance._meta.model, field_name).rel
                    try:
598
                        if rel.related_model == manager.model:
Jean-Baptiste's avatar
Jean-Baptiste committed
599 600 601 602
                            reverse_id = rel.remote_field.attname
                            item[reverse_id] = instance.pk
                    except AttributeError:
                        pass
Jean-Baptiste's avatar
Jean-Baptiste committed
603
                    saved_item = self.internal_create(validated_data=item, model=manager.model)
604

605
                if getattr(manager, 'through', None) is not None and manager.through._meta.auto_created:
Jean-Baptiste's avatar
Jean-Baptiste committed
606
                    manager.add(saved_item)