Skip to content

Commit f81d516

Browse files
authored
Robust uniqueness checks. (#4217)
* Robust uniqueness checks * Add master to test matrix (allow_failures)
1 parent a20a756 commit f81d516

File tree

3 files changed

+49
-11
lines changed

3 files changed

+49
-11
lines changed

.travis.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ env:
1919
- TOX_ENV=py27-django110
2020
- TOX_ENV=py35-django110
2121
- TOX_ENV=py34-django110
22+
- TOX_ENV=py27-djangomaster
23+
- TOX_ENV=py34-djangomaster
24+
- TOX_ENV=py35-djangomaster
2225

2326
matrix:
2427
fast_finish: true
2528
allow_failures:
26-
- TOX_ENV=py27-djangomaster
27-
- TOX_ENV=py34-djangomaster
28-
- TOX_ENV=py35-djangomaster
29+
- env: TOX_ENV=py27-djangomaster
30+
- env: TOX_ENV=py34-djangomaster
31+
- env: TOX_ENV=py35-djangomaster
2932

3033
install:
3134
# Virtualenv < 14 is required to keep the Python 3.2 builds running.

rest_framework/validators.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,32 @@
88
"""
99
from __future__ import unicode_literals
1010

11+
from django.db import DataError
1112
from django.utils.translation import ugettext_lazy as _
1213

1314
from rest_framework.compat import unicode_to_repr
1415
from rest_framework.exceptions import ValidationError
1516
from rest_framework.utils.representation import smart_repr
1617

1718

19+
# Robust filter and exist implementations. Ensures that queryset.exists() for
20+
# an invalid value returns `False`, rather than raising an error.
21+
# Refs https://github.com/tomchristie/django-rest-framework/issues/3381
22+
23+
def qs_exists(queryset):
24+
try:
25+
return queryset.exists()
26+
except (TypeError, ValueError, DataError):
27+
return False
28+
29+
30+
def qs_filter(queryset, **kwargs):
31+
try:
32+
return queryset.filter(**kwargs)
33+
except (TypeError, ValueError, DataError):
34+
return queryset.none()
35+
36+
1837
class UniqueValidator(object):
1938
"""
2039
Validator that corresponds to `unique=True` on a model field.
@@ -44,7 +63,7 @@ def filter_queryset(self, value, queryset):
4463
Filter the queryset to all instances matching the given attribute.
4564
"""
4665
filter_kwargs = {self.field_name: value}
47-
return queryset.filter(**filter_kwargs)
66+
return qs_filter(queryset, **filter_kwargs)
4867

4968
def exclude_current_instance(self, queryset):
5069
"""
@@ -59,7 +78,7 @@ def __call__(self, value):
5978
queryset = self.queryset
6079
queryset = self.filter_queryset(value, queryset)
6180
queryset = self.exclude_current_instance(queryset)
62-
if queryset.exists():
81+
if qs_exists(queryset):
6382
raise ValidationError(self.message)
6483

6584
def __repr__(self):
@@ -124,7 +143,7 @@ def filter_queryset(self, attrs, queryset):
124143
field_name: attrs[field_name]
125144
for field_name in self.fields
126145
}
127-
return queryset.filter(**filter_kwargs)
146+
return qs_filter(queryset, **filter_kwargs)
128147

129148
def exclude_current_instance(self, attrs, queryset):
130149
"""
@@ -145,7 +164,7 @@ def __call__(self, attrs):
145164
checked_values = [
146165
value for field, value in attrs.items() if field in self.fields
147166
]
148-
if None not in checked_values and queryset.exists():
167+
if None not in checked_values and qs_exists(queryset):
149168
field_names = ', '.join(self.fields)
150169
raise ValidationError(self.message.format(field_names=field_names))
151170

@@ -209,7 +228,7 @@ def __call__(self, attrs):
209228
queryset = self.queryset
210229
queryset = self.filter_queryset(attrs, queryset)
211230
queryset = self.exclude_current_instance(attrs, queryset)
212-
if queryset.exists():
231+
if qs_exists(queryset):
213232
message = self.message.format(date_field=self.date_field)
214233
raise ValidationError({self.field: message})
215234

@@ -234,7 +253,7 @@ def filter_queryset(self, attrs, queryset):
234253
filter_kwargs['%s__day' % self.date_field_name] = date.day
235254
filter_kwargs['%s__month' % self.date_field_name] = date.month
236255
filter_kwargs['%s__year' % self.date_field_name] = date.year
237-
return queryset.filter(**filter_kwargs)
256+
return qs_filter(queryset, **filter_kwargs)
238257

239258

240259
class UniqueForMonthValidator(BaseUniqueForValidator):
@@ -247,7 +266,7 @@ def filter_queryset(self, attrs, queryset):
247266
filter_kwargs = {}
248267
filter_kwargs[self.field_name] = value
249268
filter_kwargs['%s__month' % self.date_field_name] = date.month
250-
return queryset.filter(**filter_kwargs)
269+
return qs_filter(queryset, **filter_kwargs)
251270

252271

253272
class UniqueForYearValidator(BaseUniqueForValidator):
@@ -260,4 +279,4 @@ def filter_queryset(self, attrs, queryset):
260279
filter_kwargs = {}
261280
filter_kwargs[self.field_name] = value
262281
filter_kwargs['%s__year' % self.date_field_name] = date.year
263-
return queryset.filter(**filter_kwargs)
282+
return qs_filter(queryset, **filter_kwargs)

tests/test_validators.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ class Meta:
4848
fields = '__all__'
4949

5050

51+
class IntegerFieldModel(models.Model):
52+
integer = models.IntegerField()
53+
54+
55+
class UniquenessIntegerSerializer(serializers.Serializer):
56+
# Note that this field *deliberately* does not correspond with the model field.
57+
# This allows us to ensure that `ValueError`, `TypeError` or `DataError` etc
58+
# raised by a uniqueness check does not trigger a deceptive "this field is not unique"
59+
# validation failure.
60+
integer = serializers.CharField(validators=[UniqueValidator(queryset=IntegerFieldModel.objects.all())])
61+
62+
5163
class TestUniquenessValidation(TestCase):
5264
def setUp(self):
5365
self.instance = UniquenessModel.objects.create(username='existing')
@@ -100,6 +112,10 @@ def test_related_model_is_unique(self):
100112
rs = RelatedModelSerializer(data=data)
101113
self.assertTrue(rs.is_valid())
102114

115+
def test_value_error_treated_as_not_unique(self):
116+
serializer = UniquenessIntegerSerializer(data={'integer': 'abc'})
117+
assert serializer.is_valid()
118+
103119

104120
# Tests for `UniqueTogetherValidator`
105121
# -----------------------------------

0 commit comments

Comments
 (0)
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