UniqueTogetherValidator is incompatible with field source · Issue #7003 · encode/django-rest-framework (original) (raw)

The UniqueTogetherValidator is incompatible with serializer fields where the serializer field name is different than the underlying model field name. i.e., fields that have a source specified.

This manifests itself in two ways:

from django.db import models from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator

from django.test import TestCase

class ExampleModel(models.Model): field1 = models.CharField(max_length=60) field2 = models.CharField(max_length=60)

class Meta:
    unique_together = ['field1', 'field2']

class WriteableExample(serializers.ModelSerializer): alias = serializers.CharField(source='field1')

class Meta:
    model = ExampleModel
    fields = ['alias', 'field2']
    validators = [
        UniqueTogetherValidator(
            queryset=ExampleModel.objects.all(),
            fields=['alias', 'field2']
        )
    ]

class ReadOnlyExample(serializers.ModelSerializer): alias = serializers.CharField(source='field1', default='test', read_only=True)

class Meta:
    model = ExampleModel
    fields = ['alias', 'field2']
    validators = [
        UniqueTogetherValidator(
            queryset=ExampleModel.objects.all(),
            fields=['alias', 'field2']
        )
    ]

class ExampleTests(TestCase):

def test_writeable(self):
    serializer = WriteableExample(data={'alias': 'test', 'field2': 'test'})
    serializer.is_valid(raise_exception=True)

def test_read_only(self):
    serializer = ReadOnlyExample(data={'field2': 'test'})
    serializer.is_valid(raise_exception=True)

Test output

==================================================== FAILURES =====================================================
___________________________________________ ExampleTests.test_read_only ___________________________________________
tests/test_unique_together_example.py:52: in test_read_only
    serializer.is_valid(raise_exception=True)
rest_framework/serializers.py:234: in is_valid
    self._validated_data = self.run_validation(self.initial_data)
rest_framework/serializers.py:431: in run_validation
    self.run_validators(value)
rest_framework/serializers.py:464: in run_validators
    super().run_validators(to_validate)
rest_framework/fields.py:557: in run_validators
    validator(value)
rest_framework/validators.py:157: in __call__
    queryset = self.filter_queryset(attrs, queryset)
rest_framework/validators.py:143: in filter_queryset
    return qs_filter(queryset, **filter_kwargs)
rest_framework/validators.py:28: in qs_filter
    return queryset.filter(**kwargs)
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/query.py:892: in filter
    return self._filter_or_exclude(False, *args, **kwargs)
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/query.py:910: in _filter_or_exclude
    clone.query.add_q(Q(*args, **kwargs))
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/sql/query.py:1290: in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/sql/query.py:1318: in _add_q
    split_subq=split_subq, simple_col=simple_col,
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/sql/query.py:1190: in build_filter
    lookups, parts, reffed_expression = self.solve_lookup_type(arg)
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/sql/query.py:1049: in solve_lookup_type
    _, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
.tox/venvs/py37-django22/lib/python3.7/site-packages/django/db/models/sql/query.py:1420: in names_to_path
    "Choices are: %s" % (name, ", ".join(available)))
E   django.core.exceptions.FieldError: Cannot resolve keyword 'alias' into field. Choices are: field1, field2, id

___________________________________________ ExampleTests.test_writeable ___________________________________________
tests/test_unique_together_example.py:48: in test_writeable
    serializer.is_valid(raise_exception=True)
rest_framework/serializers.py:242: in is_valid
    raise ValidationError(self.errors)
E   rest_framework.exceptions.ValidationError: {'alias': [ErrorDetail(string='This field is required.', code='required')]}


Original Description

Checklist

Steps to reproduce

according to the changes in drf 3.8.2 the read_only+default fields should be explicilty saved in perform_create of viewset or create, save or update method of serializer.

this perform_create is called after validating the data by serializer.is_valid() inside create method of CreateModelMixin

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

now in my codebase i had two cases:

case1 : when the name of field of serializer is same as model like:

model:

class A(models.Model):
      a = models.ForeignKey(B, on_delete=models.CASCADE)
      c = models.CharField(max_length=32)
      ....

serializer:

 class SomeSerializer(serializers.ModelSerializer):
      a = HyperlinkedRelatedField(view_name='c-detail', read_only=True, default=<some instance of Model B>)

      class Meta:
      model=A
      fields=( 'a', 'c', ......)
      validators = [
            UniqueTogetherValidator(
                queryset=A.objects.all(),
                fields=('a', 'c')
            )
        ]

In this case serializer.is_valid() is not failing if i POST something to this serializer and my overwritten perform_create is being called so problem here

because as we can see these fields are included in _read_only_defaults after #5922 and here https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L451 the field.field_name is added in the return value that will be validated with uniquetogether so no issues come.

case 2: when the name of field of serializer is 'not' same as model's field and a source is added in the serializer field like:
model:

class A(models.Model):
      a = models.ForeignKey(B, on_delete=models.CASCADE)
      c = models.CharField(max_length=32)
      ....

serializer:

 class SomeSerializer(serializers.ModelSerializer):
      b = HyperlinkedRelatedField(source='a', view_name='c-detail', read_only=True, default=<some instance of Model B>)

      class Meta:
      model=A
      fields=( 'b', 'c', ......)
      validators = [
            UniqueTogetherValidator(
                queryset=A.objects.all(),
                fields=('a', 'c')
            )
        ]

in this case, if i POST something to this serializer, a ValidationError exception is thrown
saying
{'a': [ErrorDetail(string='This field is required.', code='required')]}

because since field.field_name is included in the return ordered dictionary as key so here in the unique together will fail as it expects ('a', 'c') but gets ('b', 'c').

Expected behavior

in second case also it should work fine

Actual behavior

throwing error {'a': [ErrorDetail(string='This field is required.', code='required')]}

Possible source of error found

Since all the fields (including read_only) were earlier present in _wriable_fields, this is the place where it used to get the field name from source - https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L496

And since it started using the method _read_only_defaults for these read_only+default fields, it populates using field name rather than source name - https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L451

which maybe a bug