Skip to content

Exclude read_only=True fields from unique_together validation & add docs. #4192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion docs/api-guide/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden

---

# Advanced 'default' argument usage
# Advanced field defaults

Validators that are applied across multiple fields in the serializer can sometimes require a field input that should not be provided by the API client, but that *is* available as input to the validator.

Expand Down Expand Up @@ -188,6 +188,71 @@ It takes a single argument, which is the default value or callable that should b

---

# Limitations of validators

There are some ambiguous cases where you'll need to instead handle validation
explicitly, rather than relying on the default serializer classes that
`ModelSerializer` generates.

In these cases you may want to disable the automatically generated validators,
by specifying an empty list for the serializer `Meta.validators` attribute.

## Optional fields

By default "unique together" validation enforces that all fields be
`required=True`. In some cases, you might want to explicit apply
`required=False` to one of the fields, in which case the desired behaviour
of the validation is ambiguous.

In this case you will typically need to exclude the validator from the
serializer class, and instead write any validation logic explicitly, either
in the `.validate()` method, or else in the view.

For example:

class BillingRecordSerializer(serializers.ModelSerializer):
def validate(self, data):
# Apply custom validation either here, or in the view.

class Meta:
fields = ('client', 'date', 'amount')
extra_kwargs = {'client' {'required': 'False'}}
validators = [] # Remove a default "unique together" constraint.

## Updating nested serializers

When applying an update to an existing instance, uniqueness validators will
exclude the current instance from the uniqueness check. The current instance
is available in the context of the uniqueness check, because it exists as
an attribute on the serializer, having initially been passed using
`instance=...` when instantiating the serializer.

In the case of update operations on *nested* serializers there's no way of
applying this exclusion, because the instance is not available.

Again, you'll probably want to explicitly remove the validator from the
serializer class, and write the code the for the validation constraint
explicitly, in a `.validate()` method, or in the view.

## Debugging complex cases

If you're not sure exactly what behavior a `ModelSerializer` class will
generate it is usually a good idea to run `manage.py shell`, and print
an instance of the serializer, so that you can inspect the fields and
validators that it automatically generates for you.

>>> serializer = MyComplexModelSerializer()
>>> print(serializer)
class MyComplexModelSerializer:
my_fields = ...

Also keep in mind that with complex cases it can often be better to explicitly
define your serializer classes, rather than relying on the default
`ModelSerializer` behavior. This involves a little more code, but ensures
that the resulting behavior is more transparent.

---

# Writing custom validators

You can use any of Django's existing validators, or write your own custom validators.
Expand Down
11 changes: 10 additions & 1 deletion rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,11 @@ def get_extra_kwargs(self):

read_only_fields = getattr(self.Meta, 'read_only_fields', None)
if read_only_fields is not None:
if not isinstance(read_only_fields, (list, tuple)):
raise TypeError(
'The `read_only_fields` option must be a list or tuple. '
'Got %s.' % type(read_only_fields).__name__
)
for field_name in read_only_fields:
kwargs = extra_kwargs.get(field_name, {})
kwargs['read_only'] = True
Expand All @@ -1258,6 +1263,9 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs

('dict of updated extra kwargs', 'mapping of hidden fields')
"""
if getattr(self.Meta, 'validators', None) is not None:
return (extra_kwargs, {})

model = getattr(self.Meta, 'model')
model_fields = self._get_model_fields(
field_names, declared_fields, extra_kwargs
Expand Down Expand Up @@ -1308,7 +1316,7 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs
else:
uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
elif default is not empty:
# The corresponding field is not present in the,
# The corresponding field is not present in the
# serializer. We have a default to use for it, so
# add in a hidden field that populates it.
hidden_fields[unique_constraint_name] = HiddenField(default=default)
Expand Down Expand Up @@ -1390,6 +1398,7 @@ def get_unique_together_validators(self):
field_names = {
field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source)
and not field.read_only
}

# Note that we make sure to check `unique_together` both on the
Expand Down
2 changes: 0 additions & 2 deletions tests/test_model_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,6 @@ class Meta:
one_to_one = NestedSerializer(read_only=True):
url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
name = CharField(max_length=100)
class Meta:
validators = [<UniqueTogetherValidator(queryset=UniqueTogetherModel.objects.all(), fields=('foreign_key', 'one_to_one'))>]
""")
if six.PY2:
# This case is also too awkward to resolve fully across both py2
Expand Down
39 changes: 39 additions & 0 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,45 @@ class Meta:
""")
assert repr(serializer) == expected

def test_ignore_read_only_fields(self):
"""
When serializer fields are read only, then uniqueness
validators should not be added for that field.
"""
class ReadOnlyFieldSerializer(serializers.ModelSerializer):
class Meta:
model = UniquenessTogetherModel
fields = ('id', 'race_name', 'position')
read_only_fields = ('race_name',)

serializer = ReadOnlyFieldSerializer()
expected = dedent("""
ReadOnlyFieldSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(read_only=True)
position = IntegerField(required=True)
""")
assert repr(serializer) == expected

def test_allow_explict_override(self):
"""
Ensure validators can be explicitly removed..
"""
class NoValidatorsSerializer(serializers.ModelSerializer):
class Meta:
model = UniquenessTogetherModel
fields = ('id', 'race_name', 'position')
validators = []

serializer = NoValidatorsSerializer()
expected = dedent("""
NoValidatorsSerializer():
id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100)
position = IntegerField()
""")
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.
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy