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

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

Thibaud's avatar
Thibaud committed
28

29
class LDListMixin:
Jean-Baptiste's avatar
Jean-Baptiste committed
30 31
    child_attr = 'child'

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

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

46
    def to_representation(self, value):
47 48 49 50 51
        '''
        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
        '''
52
        try:
53
            child_model = getattr(self, self.child_attr).Meta.model
54
        except AttributeError:
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
            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']))
76
        if not self.id.startswith('http'):
77
            self.id = '{}{}{}'.format(settings.BASE_URL, Model.resource(parent_model), self.id)
78 79
        return {'@id': self.id,
                '@type': 'ldp:Container',
80 81
                'ldp:contains': super().to_representation(filtered_values),
                'permissions': container_permissions
82
                }
Jean-Baptiste's avatar
Jean-Baptiste committed
83

84
    def get_attribute(self, instance):
85 86 87
        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
88
        self.id = parent_id + self.field_name + "/"
89
        return super().get_attribute(instance)
90

91 92
    def get_value(self, dictionary):
        try:
93 94
            object_list = dictionary['@graph']

Thibaud's avatar
Thibaud committed
95 96 97 98 99 100
            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)
101
                obj = next(filter(lambda o: container_id.lstrip('/') in o[self.parent.url_field_name], object_list))
102 103
            list = super().get_value(obj)
            try:
Thibaud's avatar
Thibaud committed
104 105
                list = next(
                    filter(lambda o: list[self.parent.url_field_name] == o[self.parent.url_field_name], object_list))
106
            except (KeyError, TypeError, StopIteration):
107 108
                pass

109
            try:
110
                list = list['ldp:contains']
111
            except (KeyError, TypeError):
112 113
                pass

Thibaud's avatar
Thibaud committed
114 115 116
            if list is empty:
                return []

117 118 119
            if isinstance(list, dict):
                list = [list]

120
            ret = []
121
            for item in list:
122
                full_item = None
123
                try:
Thibaud's avatar
Thibaud committed
124 125 126
                    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))
127 128
                except StopIteration:
                    pass
129
                if full_item is None:
130 131
                    ret.append(item)
                else:
132
                    ret.append(full_item)
133 134

            return ret
135
        except KeyError:
136 137 138 139 140 141 142 143
            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
144

Jean-Baptiste's avatar
Jean-Baptiste committed
145

146
class ContainerSerializer(LDListMixin, ListSerializer):
Jean-Baptiste's avatar
Jean-Baptiste committed
147 148
    id = ''

Sylvain Le Bon's avatar
Sylvain Le Bon committed
149 150 151
    @property
    def data(self):
        return ReturnDict(super(ListSerializer, self).data, serializer=self)
152 153 154

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

156 157
    def to_internal_value(self, data):
        try:
Thibaud's avatar
Thibaud committed
158
            return super().to_internal_value(data[self.parent.url_field_name])
159
        except (KeyError, TypeError):
160 161
            return super().to_internal_value(data)

Sylvain Le Bon's avatar
Sylvain Le Bon committed
162

163
class ManyJsonLdRelatedField(LDListMixin, ManyRelatedField):
Jean-Baptiste's avatar
Jean-Baptiste committed
164 165 166
    child_attr = 'child_relation'
    url_field_name = "@id"

167

168
class JsonLdField(HyperlinkedRelatedField):
169 170
    def __init__(self, view_name=None, **kwargs):
        super().__init__(view_name, **kwargs)
171
        self.get_lookup_args()
Jean-Baptiste's avatar
Jean-Baptiste committed
172

173
    def get_lookup_args(self):
174
        try:
175 176 177
            lookup_field = get_resolver().reverse_dict[self.view_name][0][0][1][0]
            self.lookup_field = lookup_field
            self.lookup_url_kwarg = lookup_field
178 179
        except MultiValueDictKeyError:
            pass
180

181 182 183 184 185 186
    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
187

188
class JsonLdRelatedField(JsonLdField):
189
    def to_representation(self, value):
190
        try:
Jean-Baptiste's avatar
Jean-Baptiste committed
191
            if Model.is_external(value):
192
                return {'@id': value.urlid}
Jean-Baptiste's avatar
Jean-Baptiste committed
193 194
            else:
                return {'@id': super().to_representation(value)}
195 196
        except ImproperlyConfigured:
            return value.pk
Jean-Baptiste's avatar
Jean-Baptiste committed
197

198 199
    def to_internal_value(self, data):
        try:
Thibaud's avatar
Thibaud committed
200
            return super().to_internal_value(data[self.parent.url_field_name])
201
        except (KeyError, TypeError):
202
            return super().to_internal_value(data)
Jean-Baptiste's avatar
Jean-Baptiste committed
203

204 205 206 207 208 209
    @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]
210
        return ManyJsonLdRelatedField(**list_kwargs)
211

Jean-Baptiste's avatar
Jean-Baptiste committed
212

213
class JsonLdIdentityField(JsonLdField):
214 215 216
    def __init__(self, view_name=None, **kwargs):
        kwargs['read_only'] = True
        kwargs['source'] = '*'
217
        super().__init__(view_name, **kwargs)
Jean-Baptiste's avatar
Jean-Baptiste committed
218

219 220 221
    def use_pk_only_optimization(self):
        return False

222 223
    def to_internal_value(self, data):
        try:
Thibaud's avatar
Thibaud committed
224
            return super().to_internal_value(data[self.parent.url_field_name])
225
        except KeyError:
226 227
            return super().to_internal_value(data)

228 229 230
    def get_value(self, dictionary):
        return super().get_value(dictionary)

231 232
    def to_representation(self, value: Any) -> Any:
        try:
Jean-Baptiste's avatar
Jean-Baptiste committed
233 234 235 236
            if isinstance(value, str):
                return Hyperlink(value, value)
            else:
                return Hyperlink(value.webid(), value)
237 238 239
        except AttributeError:
            return super().to_representation(value)

Jean-Baptiste's avatar
Jean-Baptiste committed
240 241 242 243 244 245 246
    def get_attribute(self, instance):
        if Model.is_external(instance):
            return instance.urlid
        else:
            return super().get_attribute(instance)


247 248 249
class LDPSerializer(HyperlinkedModelSerializer):
    url_field_name = "@id"
    serializer_related_field = JsonLdRelatedField
250
    serializer_url_field = JsonLdIdentityField
Thibaud's avatar
Thibaud committed
251
    ModelSerializer.serializer_field_mapping[LDPUrlField] = IdURLField
Jean-Baptiste's avatar
Jean-Baptiste committed
252

253
    def get_default_field_names(self, declared_fields, model_info):
254 255
        try:
            fields = list(self.Meta.model._meta.serializer_fields)
256
        except AttributeError:
257
            fields = super().get_default_field_names(declared_fields, model_info)
258
        if 'request' in self._context and not (self._context['request']._request.method == 'GET' or self._context['request']._request.method == 'OPTIONS'):
259 260 261 262 263 264
            try:
                fields.remove(self.Meta.model._meta.auto_author)
            except ValueError:
                pass
            except AttributeError:
                pass
Rob's avatar
Rob committed
265
        return fields + list(getattr(self.Meta, 'extra_fields', []))
Jean-Baptiste's avatar
Jean-Baptiste committed
266

267 268
    def to_representation(self, obj):
        data = super().to_representation(obj)
269 270
        slug_field = Model.slug_field(obj)
        for field in data:
271
            if isinstance(data[field], dict) and '@id' in data[field]:
272
                data[field]['@id'] = data[field]['@id'].format(Model.container_id(obj), str(getattr(obj, slug_field)))
Jean-Baptiste's avatar
Jean-Baptiste committed
273 274 275
        if 'urlid' in data and data['urlid'] is not None:
            data['@id'] = data.pop('urlid')['@id']
        if not '@id' in data:
276
            data['@id'] = '{}{}'.format(settings.SITE_URL, Model.resource(obj))
277 278 279 280 281 282
        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
283 284
        data['permissions'] = Model.get_permissions(obj, self.context['request'].user,
                                                    ['view', 'change', 'control', 'delete'])
285

286
        return data
Jean-Baptiste's avatar
Jean-Baptiste committed
287

288 289 290
    def build_field(self, field_name, info, model_class, nested_depth):
        return super().build_field(field_name, info, model_class, nested_depth)

291 292 293
    def build_property_field(self, field_name, model_class):
        class JSonLDPropertyField(ReadOnlyField):
            def to_representation(self, instance):
294
                from djangoldp.views import LDPViewSet
295 296 297 298

                if isinstance(instance, QuerySet) or isinstance(instance, Model):
                    try:
                        model_class = instance.model
299
                    except:
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
                        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"]

                    if isinstance(instance, QuerySet):
                        data = list(instance)

                        return {'@id': '{}{}{}/'.format(settings.SITE_URL, '{}{}/', self.source),
                                '@type': 'ldp:Container',
319 320
                                'ldp:contains': [serializer.to_representation(item) if item is not None else None for
                                                 item
321
                                                 in data],
322 323
                                'permissions': Model.get_permissions(self.parent.Meta.model,
                                                                     self.context['request'].user,
324 325 326 327
                                                                     ['view', 'add'])
                                }
                    else:
                        return serializer.to_representation(instance)
328
                else:
329
                    return instance
330 331 332 333 334 335

        field_class = JSonLDPropertyField
        field_kwargs = {}

        return field_class, field_kwargs

336 337 338 339 340 341 342 343 344
    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):
345 346
                if self.field_name == 'urlid':
                    self.field_name = '@id'
347 348
                try:
                    object_list = dictionary["@graph"]
Thibaud's avatar
Thibaud committed
349 350
                    if self.parent.instance is None:
                        obj = next(filter(
351
                            lambda o: not self.parent.url_field_name in o or "./" in o[self.parent.url_field_name],
Thibaud's avatar
Thibaud committed
352
                            object_list))
353
                        value = super().get_value(obj)
Thibaud's avatar
Thibaud committed
354 355
                    else:
                        resource_id = Model.resource_id(self.parent.instance)
Jean-Baptiste's avatar
Jean-Baptiste committed
356 357 358
                        obj = next(
                            filter(lambda o: resource_id.lstrip('/') in o[self.parent.url_field_name], object_list))
                        value = super().get_value(obj)
359
                except KeyError:
360 361
                    value = super().get_value(dictionary)

362 363 364
                if self.field_name == '@id' and value == './':
                    self.field_name = 'urlid'
                    return None
365 366 367 368 369 370 371 372 373 374 375 376
                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
377 378 379

        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())
380
        return type(field_class.__name__ + 'Valued', (JSonLDStandardField, field_class), {}), field_kwargs
381

382
    def build_nested_field(self, field_name, relation_info, nested_depth):
383

384
        class NestedLDPSerializer(self.__class__):
385

386 387 388
            class Meta:
                model = relation_info.related_model
                depth = nested_depth - 1
389
                try:
390 391
                    fields = ['@id'] + list(model._meta.serializer_fields)
                except AttributeError:
392 393
                    fields = '__all__'

Jean-Baptiste's avatar
Jean-Baptiste committed
394
            def to_internal_value(self, data):
395 396
                if data is '':
                    return ''
397
                if self.url_field_name in data:
398 399 400 401 402 403 404 405 406 407
                    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()
408

409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
                    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)

430
                    uri = data[self.url_field_name]
431 432 433 434 435 436 437 438
                    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):]

439 440
                    try:
                        match = resolve(uri_to_iri(uri))
441 442
                        slug_field = Model.slug_field(self.__class__.Meta.model)
                        ret[slug_field] = match.kwargs[slug_field]
443
                    except Resolver404:
444 445
                        if 'urlid' in data:
                            ret['urlid'] = data['urlid']
446

447
                else:
448 449 450 451 452 453
                    ret = super().to_internal_value(data)

                if self.url_field_name in data and not 'urlid' in data and data[self.url_field_name].startswith('http'):
                    ret['urlid'] = data[self.url_field_name]

                return ret
Jean-Baptiste's avatar
Jean-Baptiste committed
454 455 456

        kwargs = get_nested_relation_kwargs(relation_info)
        kwargs['read_only'] = False
457
        kwargs['required'] = False
Jean-Baptiste's avatar
Jean-Baptiste committed
458 459
        return NestedLDPSerializer, kwargs

Sylvain Le Bon's avatar
Sylvain Le Bon committed
460 461
    @classmethod
    def many_init(cls, *args, **kwargs):
462
        kwargs['child'] = cls(**kwargs)
463 464 465
        serializer = ContainerSerializer(*args, **kwargs)
        if 'context' in kwargs and getattr(kwargs['context']['view'], 'nested_field', None) is not None:
            serializer.id = '{}{}/'.format(serializer.id, kwargs['context']['view'].nested_field)
466 467
        elif 'context' in kwargs:
            serializer.id = '{}{}'.format(settings.BASE_URL, kwargs['context']['view'].request.path)
468
        return serializer
Jean-Baptiste's avatar
Jean-Baptiste committed
469

470
    def to_internal_value(self, data):
471 472
        user_case = self.Meta.model is get_user_model() and '@id' in data and not data['@id'].startswith(
            settings.BASE_URL)
473 474 475 476 477 478 479
        if user_case:
            data['username'] = 'external'
        ret = super().to_internal_value(data)
        if user_case:
            ret['username'] = data['@id']
        return ret

480
    def get_value(self, dictionary):
481 482
        try:
            object_list = dictionary["@graph"]
483 484 485 486 487 488
            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)
489
                obj = next(filter(lambda o: container_id.lstrip('/') in o[self.url_field_name], object_list))
490 491
            item = super().get_value(obj)
            full_item = None
492 493
            if item is empty:
                return empty
494
            try:
Jean-Baptiste's avatar
Jean-Baptiste committed
495 496 497
                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))
498 499 500 501 502 503 504 505 506
            except StopIteration:
                pass
            if full_item is None:
                return item
            else:
                return full_item

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

Jean-Baptiste's avatar
Jean-Baptiste committed
508
    def create(self, validated_data):
Jean-Baptiste's avatar
Jean-Baptiste committed
509 510 511 512 513 514 515
        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):
516
        model_class = self.Meta.model
Jean-Baptiste's avatar
Jean-Baptiste committed
517

518
        info = model_meta.get_field_info(model_class)
Jean-Baptiste's avatar
Jean-Baptiste committed
519 520
        many_to_many = {}
        for field_name, relation_info in info.relations.items():
521
            if relation_info.to_many and relation_info.reverse and not field_name is None:
Jean-Baptiste's avatar
Jean-Baptiste committed
522 523 524 525
                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)
526 527

    def internal_create(self, validated_data, model):
528
        validated_data = self.resolve_fk_instances(model, validated_data)
Jean-Baptiste's avatar
Jean-Baptiste committed
529

530
        nested_fields = []
Jean-Baptiste's avatar
Jean-Baptiste committed
531 532
        nested_list_fields_name = list(filter(lambda key: isinstance(validated_data[key], list), validated_data))
        for field_name in nested_list_fields_name:
533
            nested_fields.append((field_name, validated_data.pop(field_name)))
534 535 536 537 538 539 540

        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)))
541
        validated_data = self.remove_empty_value(validated_data)
542 543
        if model is get_user_model() and 'urlid' in validated_data and not 'username' in validated_data:
            validated_data['username'] = validated_data.pop('urlid')
544
        instance = model.objects.create(**validated_data)
545

546 547 548
        for field_name, value in many_to_many:
            validated_data[field_name] = value

549
        self.save_or_update_nested_list(instance, nested_fields)
Jean-Baptiste's avatar
Jean-Baptiste committed
550

551
        return instance
552

553 554 555 556 557 558
    def remove_empty_value(self, validated_data):
        for attr, value in validated_data.items():
            if value is '':
                validated_data[attr] = None
        return validated_data

559
    def update(self, instance, validated_data):
560 561 562
        model = self.Meta.model
        validated_data = self.resolve_fk_instances(model, validated_data)

563 564 565 566 567 568
        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():
569
            if isinstance(value, dict):
570
                value = self.update_dict_value(attr, instance, value)
571 572 573 574
            if value is '' and not isinstance(getattr(instance, attr), str):
                setattr(instance, attr, None)
            else:
                setattr(instance, attr, value)
575

576
        self.save_or_update_nested_list(instance, nested_fields)
577
        instance.save()
578 579 580

        return instance

581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
    def resolve_fk_instances(self, model, validated_data):
        nested_fk_fields_name = list(filter(lambda key: isinstance(validated_data[key], dict), validated_data))
        for field_name in nested_fk_fields_name:
            field_dict = validated_data[field_name]
            try:
                field_model = getattr(model, field_name).field.rel.model
            except:
                # not fk
                continue
            slug_field = Model.slug_field(field_model)
            sub_inst = None
            if slug_field in field_dict:
                kwargs = {slug_field: field_dict[slug_field]}
                sub_inst = field_model.objects.get(**kwargs)
            elif 'urlid' in field_dict and settings.BASE_URL in field_dict['urlid']:
                model, sub_inst = Model.resolve(field_dict['urlid'])
            elif 'urlid' in field_dict and issubclass(field_model, AbstractUser):
                kwargs = {'username': field_dict['urlid']}
                sub_inst = field_model.objects.get(**kwargs)
            elif 'urlid' in field_dict:
                kwargs = {'urlid': field_dict['urlid']}
                sub_inst = field_model.objects.get(**kwargs)
            if sub_inst is None:
                sub_inst = self.internal_create(field_dict, field_model)
            validated_data[field_name] = sub_inst
        return validated_data

608 609 610 611 612 613 614 615 616 617 618 619 620 621
    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:
622 623 624 625 626 627 628 629 630 631 632 633 634 635
            try:
                reverse_attr_name = instance._meta.fields_map[attr].remote_field.name
                many = False
            except:
                rel = list(filter(lambda field: field.name == attr, instance._meta.fields))[0].rel
                many = rel.one_to_many
                reverse_attr_name = rel.related_name
            if many:
                value[reverse_attr_name] = [instance]
                oldObj = rel.model.object.get(id=value['urlid'])
            else:
                value[reverse_attr_name] = instance
                oldObj = getattr(instance, attr, None)

636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
            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

653
    def save_or_update_nested_list(self, instance, nested_fields):
654
        for (field_name, data) in nested_fields:
655
            manager = getattr(instance, field_name)
656
            field_model = manager.model
657
            slug_field = Model.slug_field(manager.model)
Jean-Baptiste's avatar
Jean-Baptiste committed
658 659 660
            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
661 662
                item_pk_to_keep = list(
                    map(lambda e: getattr(e, slug_field), filter(lambda x: hasattr(x, slug_field), data)))
663

664 665 666
            if getattr(manager, 'through', None) is None:
                for item in list(manager.all()):
                    if not str(getattr(item, slug_field)) in item_pk_to_keep:
667
                        item.delete()
668 669
            else:
                manager.clear()
670

671
            for item in data:
Jean-Baptiste's avatar
Jean-Baptiste committed
672 673 674 675
                if not isinstance(item, dict):
                    item.save()
                    saved_item = item
                elif slug_field in item:
676
                    kwargs = {slug_field: item[slug_field]}
677 678 679 680
                    saved_item = self.get_or_create(field_model, item, kwargs)
                elif 'urlid' in item and settings.BASE_URL in item['urlid']:
                    model, old_obj = Model.resolve(item['urlid'])
                    if old_obj is not None:
681
                        saved_item = self.update(instance=old_obj, validated_data=item)
682 683 684 685 686 687 688 689
                    else:
                        saved_item = self.internal_create(validated_data=item, model=field_model)
                elif 'urlid' in item and issubclass(field_model, AbstractUser):
                    kwargs = {'username': item['urlid']}
                    saved_item = self.get_or_create(field_model, item, kwargs)
                elif 'urlid' in item:
                    kwargs = {'urlid': item['urlid']}
                    saved_item = self.get_or_create(field_model, item, kwargs)
690
                else:
Jean-Baptiste's avatar
Jean-Baptiste committed
691 692
                    rel = getattr(instance._meta.model, field_name).rel
                    try:
693
                        if rel.related_model == manager.model:
Jean-Baptiste's avatar
Jean-Baptiste committed
694 695 696 697
                            reverse_id = rel.remote_field.attname
                            item[reverse_id] = instance.pk
                    except AttributeError:
                        pass
Jean-Baptiste's avatar
Jean-Baptiste committed
698
                    saved_item = self.internal_create(validated_data=item, model=manager.model)
699

700
                if getattr(manager, 'through', None) is not None and manager.through._meta.auto_created:
701
                    manager.remove(saved_item)
Jean-Baptiste's avatar
Jean-Baptiste committed
702
                    manager.add(saved_item)
703 704 705 706 707 708 709 710

    def get_or_create(self, field_model, item, kwargs):
        try:
            old_obj = field_model.objects.get(**kwargs)
            saved_item = self.update(instance=old_obj, validated_data=item)
        except field_model.DoesNotExist:
            saved_item = self.internal_create(validated_data=item, model=field_model)
        return saved_item