diff options
| -rw-r--r-- | rest_framework/validators.py | 7 | ||||
| -rw-r--r-- | tests/test_validators.py | 54 | 
2 files changed, 60 insertions, 1 deletions
| diff --git a/rest_framework/validators.py b/rest_framework/validators.py index e3719b8d..ab361614 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -138,7 +138,12 @@ class UniqueTogetherValidator:          queryset = self.queryset          queryset = self.filter_queryset(attrs, queryset)          queryset = self.exclude_current_instance(attrs, queryset) -        if queryset.exists(): + +        # Ignore validation if any field is None +        checked_values = [ +            value for field, value in attrs.items() if field in self.fields +        ] +        if None not in checked_values and queryset.exists():              field_names = ', '.join(self.fields)              raise ValidationError(self.message.format(field_names=field_names)) diff --git a/tests/test_validators.py b/tests/test_validators.py index 072cec36..127ec6f8 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -83,11 +83,37 @@ class UniquenessTogetherModel(models.Model):          unique_together = ('race_name', 'position') +class NullUniquenessTogetherModel(models.Model): +    """ +    Used to ensure that null values are not included when checking +    unique_together constraints. + +    Ignoring items which have a null in any of the validated fields is the same +    behavior that database backends will use when they have the +    unique_together constraint added. + +    Example case: a null position could indicate a non-finisher in the race, +    there could be many non-finishers in a race, but all non-NULL +    values *should* be unique against the given `race_name`. +    """ +    date_of_birth = models.DateField(null=True)  # Not part of the uniqueness constraint +    race_name = models.CharField(max_length=100) +    position = models.IntegerField(null=True) + +    class Meta: +        unique_together = ('race_name', 'position') + +  class UniquenessTogetherSerializer(serializers.ModelSerializer):      class Meta:          model = UniquenessTogetherModel +class NullUniquenessTogetherSerializer(serializers.ModelSerializer): +    class Meta: +        model = NullUniquenessTogetherModel + +  class TestUniquenessTogetherValidation(TestCase):      def setUp(self):          self.instance = UniquenessTogetherModel.objects.create( @@ -182,6 +208,34 @@ class TestUniquenessTogetherValidation(TestCase):          """)          assert repr(serializer) == expected +    def test_ignore_validation_for_null_fields(self): +        # None values that are on fields which are part of the uniqueness +        # constraint cause the instance to ignore uniqueness validation. +        NullUniquenessTogetherModel.objects.create( +            date_of_birth=datetime.date(2000, 1, 1), +            race_name='Paris Marathon', +            position=None +        ) +        data = { +            'date': datetime.date(2000, 1, 1), +            'race_name': 'Paris Marathon', +            'position': None +        } +        serializer = NullUniquenessTogetherSerializer(data=data) +        assert serializer.is_valid() + +    def test_do_not_ignore_validation_for_null_fields(self): +        # None values that are not on fields part of the uniqueness constraint +        # do not cause the instance to skip validation. +        NullUniquenessTogetherModel.objects.create( +            date_of_birth=datetime.date(2000, 1, 1), +            race_name='Paris Marathon', +            position=1 +        ) +        data = {'date': None, 'race_name': 'Paris Marathon', 'position': 1} +        serializer = NullUniquenessTogetherSerializer(data=data) +        assert not serializer.is_valid() +  # Tests for `UniqueForDateValidator`  # ---------------------------------- | 
