Skip to content

Commit 81122e6

Browse files
committed
validators place the validated values at field.source
1 parent 124f1d6 commit 81122e6

1 file changed

Lines changed: 72 additions & 25 deletions

File tree

drf_writable_nested/mixins.py

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# -*- coding: utf-8 -*-
2+
import logging
23
from collections import OrderedDict, defaultdict
34

45
from django.contrib.contenttypes.fields import GenericRelation
56
from django.contrib.contenttypes.models import ContentType
67
from django.db import transaction
78
from django.db.models import ProtectedError, FieldDoesNotExist, OneToOneRel
8-
from django.db.models.fields.related import ForeignObjectRel, OneToOneField, ManyToManyField
9+
from django.db.models.fields.related import ForeignObjectRel, ManyToManyField
910
from django.utils.translation import ugettext_lazy as _
1011
from rest_framework import serializers
1112
from rest_framework.exceptions import ValidationError
@@ -429,6 +430,10 @@ def update(self, instance, validated_data):
429430

430431
class FieldLookupMixin(serializers.Serializer):
431432

433+
def __init__(self, *args, **kwargs):
434+
self.logger = logging.getLogger(self.__class__.__name__)
435+
super(FieldLookupMixin, self).__init__(*args, **kwargs)
436+
432437
def _get_model_field(self, source):
433438
"""Returns the field on the model"""
434439
# for serializers like ModelSerializer, the Meta.model can be used to classify fields
@@ -472,6 +477,8 @@ def _populate_field_types(self):
472477
self._cache_field_types = {}
473478
self._cache_field_sources = {}
474479
for field_name, field in self.fields.items():
480+
if isinstance(self._get_model_field(field.source), GenericRelation):
481+
raise TypeError("GenericRelation not currently supported")
475482
if field.read_only:
476483
self._cache_field_types[field_name] = self.TYPE_READ_ONLY
477484
self._cache_field_sources[field.source] = self.TYPE_READ_ONLY
@@ -506,7 +513,9 @@ class RelatedSaveMixin(FieldLookupMixin):
506513

507514
def run_validation(self, data=empty):
508515
"""Cache nested call to `to_representation` on _validate_data for use when saving."""
516+
self.logger.debug("{} validating: {}".format(self.__class__.__name__, data))
509517
self._validated_data = super(RelatedSaveMixin, self).run_validation(data)
518+
self.logger.debug("{} validated: {}".format(self.__class__.__name__, self._validated_data))
510519
self._errors = {}
511520
return self._validated_data
512521

@@ -538,9 +547,9 @@ def validated_data(self):
538547
if k not in self.field_sources or self.field_sources[k] != self.TYPE_REVERSE}
539548

540549
def save(self, **kwargs):
541-
"""We already converted the inputs into a model so we need to save that model"""
550+
"""Convert validated data into related objects and save."""
542551
# Create or update direct relations (foreign key, one-to-one)
543-
print("RelatedSaveMixin.save for {}".format(self.__class__.__name__))
552+
print("RelatedSaveMixin.save for {} with data, kwargs: {}, {}".format(self.__class__.__name__, self._validated_data, kwargs))
544553
self._save_direct_relations(kwargs=kwargs)
545554
instance = super(RelatedSaveMixin, self).save(**kwargs)
546555
self._save_reverse_relations(instance=instance, kwargs=kwargs)
@@ -551,14 +560,14 @@ def _save_direct_relations(self, kwargs):
551560
for field_name, field in self.fields.items():
552561
if self.field_types[field_name] != self.TYPE_DIRECT:
553562
continue
554-
print("{} direct save {}".format(self.__class__.__name__, field_name))
555-
if self._validated_data.get(field_name, empty) == empty and kwargs.get(field_name, empty) == empty:
563+
print("{} direct save {} with data, kwargs; {}, {}".format(self.__class__.__name__, field_name, self._validated_data.get(field.source, empty), kwargs.get(field_name, {})))
564+
if self._validated_data.get(field.source, empty) == empty and kwargs.get(field_name, empty) == empty:
556565
continue # nothing to save
557566
#if self._validated_data.get(field_name) is None or kwargs.get(field_name) is None:
558567
# continue # delete existing objects
559568
# we need to pop from kwargs so the value doesn't "overwrite" the value generated by save
560-
self._validated_data[field_name] = field.save(**kwargs.pop(field_name, {}))
561-
print("{}._validated_data[{}] to direct {}".format(self.__class__.__name__, field_name, self._validated_data[field_name]))
569+
self._validated_data[field.source] = field.save(**kwargs.pop(field_name, {}))
570+
print("{}._validated_data[{}] set to direct {}".format(self.__class__.__name__, field_name, self._validated_data[field.source]))
562571

563572
def _format_generic_lookup(self, instance, related_field):
564573
return {
@@ -571,20 +580,23 @@ def _save_reverse_relations(self, instance, kwargs):
571580
for field_name, field in self.fields.items():
572581
if self.field_types[field_name] != self.TYPE_REVERSE:
573582
continue
574-
if self._validated_data.get(field_name, empty) == empty and kwargs.get(field_name, empty) == empty:
583+
if self._validated_data.get(field.source, empty) == empty and kwargs.get(field_name, empty) == empty:
575584
continue # nothing to save
576585
# inject the instance into reverse relations so the <parent>_id ForeignKey field is valid when saved
577586
related_field = self._get_model_field(field.source).field
587+
print("{} populating reverse field {}".format(self.__class__.__name__, related_field.name))
578588
if isinstance(field, serializers.ListSerializer):
579589
for obj in field._validated_data:
580590
obj[related_field.name] = instance
581591
elif isinstance(field, serializers.ModelSerializer):
592+
if field._validated_data is None:
593+
field._validated_data = {} # delete situation, but need a place to put FK
582594
field._validated_data[related_field.name] = instance
583595
else:
584596
raise Exception("unexpected serializer type")
585597
# no tests fail if we do not cache this value in _validated_data, but it's consistent with forward relations
586-
self._validated_data[field_name] = field.save(**kwargs.get(field_name, {}))
587-
print("{}._validated_data[{}] to reverse {}".format(self.__class__.__name__, field_name, self._validated_data[field_name]))
598+
self._validated_data[field.source] = field.save(**kwargs.get(field_name, {}))
599+
print("{}._validated_data[{}] to reverse {}".format(self.__class__.__name__, field_name, self._validated_data[field.source]))
588600

589601

590602
class FocalSaveMixin(FieldLookupMixin):
@@ -594,12 +606,25 @@ class FocalSaveMixin(FieldLookupMixin):
594606
'incorrect_type': _('Nested field received an incorrect data type ({data_type}): {exception_message}'),
595607
}
596608

609+
def to_internal_value(self, data):
610+
"""Injects the PK of this field into reverse relations so they validate when created in to_internal_value."""
611+
# to_internal_value only preserves writable fields and match_on may need read-only like PK
612+
self._validated_data = super(FocalSaveMixin, self).to_internal_value(data)
613+
# patch read-only fields for match_on
614+
for field_name, field in self.fields.items():
615+
if not field.read_only:
616+
continue
617+
if field_name not in data:
618+
continue
619+
self._validated_data[field.source] = field.to_internal_value(data[field_name])
620+
return self._validated_data
621+
597622
def build_match_on(self, kwargs):
598623
match_on = {}
599624
for field_name, field in self.fields.items():
600625
if self.match_on == '__all__' or field_name in self.match_on:
601626
# build match_on dict
602-
match_on[field.source or field_name] = kwargs.get(field_name, self._validated_data.get(field_name))
627+
match_on[field.source] = kwargs.get(field_name, self._validated_data.get(field.source))
603628
# a parent serializer may inject a value that isn't among the fields, but is in `match_on`
604629
for key in self.match_on:
605630
if key not in self.fields.keys():
@@ -613,20 +638,22 @@ def build_direct_values(self, kwargs):
613638
continue # m2m fields
614639
elif self.field_types[field_name] == self.TYPE_LOCAL:
615640
# need to check kwargs dict since there's no pre-processing
616-
values[field_name] = kwargs.get(field_name, self._validated_data.get(field_name))
641+
values[field.source] = kwargs.get(field_name, self._validated_data.get(field.source))
617642
elif self.field_types[field_name] == self.TYPE_DIRECT:
618643
# kwargs should have been injected into _validated_data when direct relations were saved
619-
values[field_name] = self._validated_data.get(field_name)
644+
values[field.source] = self._validated_data.get(field.source)
620645
# reverse relations aren't sent to a create
621646
return values
622647

623648
def match(self, kwargs):
624-
print("FocalSaveMixin.match with no super")
649+
print("FocalSaveMixin.match with no super and kwargs {}".format(kwargs))
625650
return self.instance
626651

627652
@transaction.atomic
628653
def save(self, **kwargs):
629-
print("FocalSaveMixin.save for {}".format(self.__class__.__name__))
654+
print("FocalSaveMixin.save for {} with data, kwargs {}".format(self.__class__.__name__, self._validated_data, kwargs))
655+
if self._validated_data is None and kwargs == {}:
656+
return None # deleted
630657
match = self.match(kwargs)
631658
print("Match: {}".format(match))
632659
self.do_update(match, kwargs)
@@ -648,6 +675,10 @@ def do_m2m_update(self, match, kwargs):
648675
class NestedSaveListSerializer(serializers.ListSerializer):
649676
"""Need a special save() method that cascades to the list of child instances"""
650677

678+
def __init__(self, *args, **kwargs):
679+
self.logger = logging.getLogger(self.__class__.__name__)
680+
super(NestedSaveListSerializer, self).__init__(*args, **kwargs)
681+
651682
def save(self, **kwargs):
652683
"""
653684
Save and return a list of object instances.
@@ -661,24 +692,27 @@ def save(self, **kwargs):
661692
"need to set extra attributes on the saved model instance. "
662693
"For example: 'serializer.save(owner=request.user)'.'"
663694
)
664-
print("List-{} save: {}, {}".format(self.child.__class__.__name__, self._validated_data, kwargs))
695+
print("List-{} save with data, kwargs: {}, {}".format(self.child.__class__.__name__, self._validated_data, kwargs))
665696

666697
new_values = []
667698

668699
for item in self._validated_data:
669700
# integrate save kwargs
670701
self.child._validated_data = item
671-
# since we reuse the serializer, we need to re-inject the new _validated_data using save kwargs
672-
new_values.append(self.child.save(**kwargs))
702+
new_value = self.child.save(**kwargs)
703+
# None indicates a deleted item
704+
if new_value is not None:
705+
new_values.append(new_value)
673706

674707
print("List-{} saved: {}".format(self.child.__class__.__name__, new_values))
675708
return new_values
676709

677710
def run_validation(self, data=empty):
678711
"""Since a nested serializer is treated like a Field, `is_valid` will not be called so we need to set
679712
_validated_data in the mixin."""
713+
self.logger.debug("List-{} validating: {}".format(self.child.__class__.__name__, data))
680714
self._validated_data = super(NestedSaveListSerializer, self).run_validation(data)
681-
print("List-{} validated: {}".format(self.child.__class__.__name__, self._validated_data))
715+
self.logger.debug("List-{} validated: {}".format(self.child.__class__.__name__, self._validated_data))
682716
return self._validated_data
683717

684718

@@ -707,6 +741,8 @@ class Meta:
707741
return super(NestedSaveSerializer, cls).many_init(*args, **kwargs)
708742

709743
def __init__(self, *args, **kwargs):
744+
if kwargs.get('partial', False) is True:
745+
raise ValueError("Partial Updates not currently supported by NestedSaveSerializer")
710746
self.queryset = kwargs.pop('queryset', self.queryset)
711747
if self.queryset is None and hasattr(self, 'Meta') and hasattr(self.Meta, 'model'):
712748
self.queryset = self.Meta.model.objects.all()
@@ -722,9 +758,11 @@ def __init__(self, *args, **kwargs):
722758

723759
def run_validation(self, data=empty):
724760
"""A nested serializer is treated like a Field so `is_valid` will not be called and `_validated_data` not set."""
761+
self.logger.debug("{} validating: {}".format(self.__class__.__name__, data))
725762
# ensure Unique and UniqueTogether don't collide with a DB match
726763
validators = self.remove_validation_unique()
727764
self._validated_data = super(NestedSaveSerializer, self).run_validation(data)
765+
self.logger.debug("{} validated: {}".format(self.__class__.__name__, self._validated_data))
728766
# restore Unique or UniqueTogether
729767
self.restore_validation_unique(validators)
730768
return self._validated_data
@@ -763,6 +801,10 @@ def restore_validation_unique(self, unique_validators):
763801
for validator in validators:
764802
fields[name].validators.append(validator)
765803

804+
def save(self, **kwargs):
805+
print("NestedSaveSerializer.save with data, kwargs: {}, {}".format(self._validated_data, kwargs))
806+
return super().save(**kwargs)
807+
766808
def update(self, instance, validated_data):
767809
raise KeyError(
768810
"Update should never be called on a NestedSerializerBase. Make sure parent object uses NestedSaveMixin")
@@ -773,6 +815,7 @@ def create(self, validated_data):
773815

774816

775817
class UpdateDoSaveMixin(NestedSaveSerializer):
818+
"""Adds behavior to update the focal object, including M2M relations"""
776819

777820
def do_update(self, match, kwargs):
778821
update_values = self.build_direct_values(kwargs)
@@ -787,17 +830,21 @@ def do_m2m_update(self, match, kwargs):
787830
# we don't care whether it's forward or reverse
788831
# if we provide a custom serializer, it may not inherit from ManyRelatedField
789832
if isinstance(field, ManyRelatedField) or isinstance(self._get_model_field(field.source), ManyToManyField):
790-
value = kwargs.get(field_name, self._validated_data.get(field_name))
791-
print("{}: m2m set to {}, {}".format(self.__class__.__name__, field_name, value))
792-
getattr(match, field_name).set(value)
833+
value = kwargs.get(field_name, self._validated_data.get(field.source, empty))
834+
if value is empty:
835+
continue # no information
836+
if value is None:
837+
value = [] # explicitly clear
838+
print("{}: m2m set to {}, {}".format(self.__class__.__name__, field.source, value))
839+
getattr(match, field.source).set(value)
793840

794841

795842
class GetOnlyNestedSerializerMixin(NestedSaveSerializer):
796843
"""Gets (without updating) requetsed object or fails."""
797844

798845
def match(self, kwargs):
799846
match = super(GetOnlyNestedSerializerMixin, self).match(kwargs)
800-
print("GetOnlyNestedSerializerMixin.match with super: {}".format(match))
847+
print("GetOnlyNestedSerializerMixin.match with super {} and kwargs {}".format(match, kwargs))
801848
if match is not None:
802849
return match
803850
try:
@@ -817,7 +864,7 @@ class GetOrCreateNestedSerializerMixin(GetOnlyNestedSerializerMixin):
817864

818865
def match(self, kwargs):
819866
match = super(GetOrCreateNestedSerializerMixin, self).match(kwargs)
820-
print("GetOrCreateNestedSerializerMixin.match with super: {}".format(match))
867+
print("GetOrCreateNestedSerializerMixin.match with super {} and kwargs {}".format(match, kwargs))
821868
if match is not None:
822869
return match
823870
create_values = self.build_direct_values(kwargs)
@@ -832,7 +879,7 @@ class CreateOnlyNestedSerializerMixin(NestedSaveSerializer):
832879
"""Creates requested object or fails."""
833880

834881
def match(self, kwargs):
835-
print("CreateOnlyNestedSerializerMixin.match with no super")
882+
print("GetOrCreateNestedSerializerMixin.match with no super and kwargs {}".format(kwargs))
836883
create_values = self.build_direct_values(kwargs)
837884
return self.queryset.model(**create_values)
838885

0 commit comments

Comments
 (0)