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)