Skip to content

Commit f387cd8

Browse files
committed
Uniqueness constraints imply a forced 'required=True'. Refs #1945
1 parent 93633c2 commit f387cd8

File tree

3 files changed

+100
-16
lines changed

3 files changed

+100
-16
lines changed

docs/api-guide/validators.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ The validator should be applied to *serializer classes*, like so:
9393
)
9494
]
9595

96+
---
97+
98+
**Note**: The `UniqueTogetherValidation` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
99+
100+
---
101+
96102
## UniqueForDateValidator
97103

98104
## UniqueForMonthValidator
@@ -146,6 +152,10 @@ If you want the date field to be entirely hidden from the user, then use `Hidden
146152

147153
---
148154

155+
**Note**: The `UniqueFor<Range>Validation` classes always imposes an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
156+
157+
---
158+
149159
# Writing custom validators
150160

151161
You can use any of Django's existing validators, or write your own custom validators.

rest_framework/validators.py

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def __init__(self, queryset, message=None):
2525
self.message = message or self.message
2626

2727
def set_context(self, serializer_field):
28+
"""
29+
This hook is called by the serializer instance,
30+
prior to the validation call being made.
31+
"""
2832
# Determine the underlying model field name. This may not be the
2933
# same as the serializer field name if `source=<>` is set.
3034
self.field_name = serializer_field.source_attrs[0]
@@ -54,6 +58,7 @@ class UniqueTogetherValidator:
5458
Should be applied to the serializer class, not to an individual field.
5559
"""
5660
message = _('The fields {field_names} must make a unique set.')
61+
missing_message = _('This field is required.')
5762

5863
def __init__(self, queryset, fields, message=None):
5964
self.queryset = queryset
@@ -62,17 +67,49 @@ def __init__(self, queryset, fields, message=None):
6267
self.message = message or self.message
6368

6469
def set_context(self, serializer):
70+
"""
71+
This hook is called by the serializer instance,
72+
prior to the validation call being made.
73+
"""
6574
# Determine the existing instance, if this is an update operation.
6675
self.instance = getattr(serializer, 'instance', None)
6776

68-
def __call__(self, attrs):
69-
# Ensure uniqueness.
77+
def enforce_required_fields(self, attrs):
78+
"""
79+
The `UniqueTogetherValidator` always forces an implied 'required'
80+
state on the fields it applies to.
81+
"""
82+
missing = dict([
83+
(field_name, self.missing_message)
84+
for field_name in self.fields
85+
if field_name not in attrs
86+
])
87+
if missing:
88+
raise ValidationError(missing)
89+
90+
def filter_queryset(self, attrs, queryset):
91+
"""
92+
Filter the queryset to all instances matching the given attributes.
93+
"""
7094
filter_kwargs = dict([
7195
(field_name, attrs[field_name]) for field_name in self.fields
7296
])
73-
queryset = self.queryset.filter(**filter_kwargs)
97+
return queryset.filter(**filter_kwargs)
98+
99+
def exclude_current_instance(self, attrs, queryset):
100+
"""
101+
If an instance is being updated, then do not include
102+
that instance itself as a uniqueness conflict.
103+
"""
74104
if self.instance is not None:
75-
queryset = queryset.exclude(pk=self.instance.pk)
105+
return queryset.exclude(pk=self.instance.pk)
106+
return queryset
107+
108+
def __call__(self, attrs):
109+
self.enforce_required_fields(attrs)
110+
queryset = self.queryset
111+
queryset = self.filter_queryset(attrs, queryset)
112+
queryset = self.exclude_current_instance(attrs, queryset)
76113
if queryset.exists():
77114
field_names = ', '.join(self.fields)
78115
raise ValidationError(self.message.format(field_names=field_names))
@@ -87,6 +124,7 @@ def __repr__(self):
87124

88125
class BaseUniqueForValidator:
89126
message = None
127+
missing_message = _('This field is required.')
90128

91129
def __init__(self, queryset, field, date_field, message=None):
92130
self.queryset = queryset
@@ -95,22 +133,47 @@ def __init__(self, queryset, field, date_field, message=None):
95133
self.message = message or self.message
96134

97135
def set_context(self, serializer):
136+
"""
137+
This hook is called by the serializer instance,
138+
prior to the validation call being made.
139+
"""
98140
# Determine the underlying model field names. These may not be the
99141
# same as the serializer field names if `source=<>` is set.
100142
self.field_name = serializer.fields[self.field].source_attrs[0]
101143
self.date_field_name = serializer.fields[self.date_field].source_attrs[0]
102144
# Determine the existing instance, if this is an update operation.
103145
self.instance = getattr(serializer, 'instance', None)
104146

105-
def get_filter_kwargs(self, attrs):
106-
raise NotImplementedError('`get_filter_kwargs` must be implemented.')
147+
def enforce_required_fields(self, attrs):
148+
"""
149+
The `UniqueFor<Range>Validator` classes always force an implied
150+
'required' state on the fields they are applied to.
151+
"""
152+
missing = dict([
153+
(field_name, self.missing_message)
154+
for field_name in [self.field, self.date_field]
155+
if field_name not in attrs
156+
])
157+
if missing:
158+
raise ValidationError(missing)
107159

108-
def __call__(self, attrs):
109-
filter_kwargs = self.get_filter_kwargs(attrs)
160+
def filter_queryset(self, attrs, queryset):
161+
raise NotImplementedError('`filter_queryset` must be implemented.')
110162

111-
queryset = self.queryset.filter(**filter_kwargs)
163+
def exclude_current_instance(self, attrs, queryset):
164+
"""
165+
If an instance is being updated, then do not include
166+
that instance itself as a uniqueness conflict.
167+
"""
112168
if self.instance is not None:
113-
queryset = queryset.exclude(pk=self.instance.pk)
169+
return queryset.exclude(pk=self.instance.pk)
170+
return queryset
171+
172+
def __call__(self, attrs):
173+
self.enforce_required_fields(attrs)
174+
queryset = self.queryset
175+
queryset = self.filter_queryset(attrs, queryset)
176+
queryset = self.exclude_current_instance(attrs, queryset)
114177
if queryset.exists():
115178
message = self.message.format(date_field=self.date_field)
116179
raise ValidationError({self.field: message})
@@ -127,7 +190,7 @@ def __repr__(self):
127190
class UniqueForDateValidator(BaseUniqueForValidator):
128191
message = _('This field must be unique for the "{date_field}" date.')
129192

130-
def get_filter_kwargs(self, attrs):
193+
def filter_queryset(self, attrs, queryset):
131194
value = attrs[self.field]
132195
date = attrs[self.date_field]
133196

@@ -136,30 +199,30 @@ def get_filter_kwargs(self, attrs):
136199
filter_kwargs['%s__day' % self.date_field_name] = date.day
137200
filter_kwargs['%s__month' % self.date_field_name] = date.month
138201
filter_kwargs['%s__year' % self.date_field_name] = date.year
139-
return filter_kwargs
202+
return queryset.filter(**filter_kwargs)
140203

141204

142205
class UniqueForMonthValidator(BaseUniqueForValidator):
143206
message = _('This field must be unique for the "{date_field}" month.')
144207

145-
def get_filter_kwargs(self, attrs):
208+
def filter_queryset(self, attrs, queryset):
146209
value = attrs[self.field]
147210
date = attrs[self.date_field]
148211

149212
filter_kwargs = {}
150213
filter_kwargs[self.field_name] = value
151214
filter_kwargs['%s__month' % self.date_field_name] = date.month
152-
return filter_kwargs
215+
return queryset.filter(**filter_kwargs)
153216

154217

155218
class UniqueForYearValidator(BaseUniqueForValidator):
156219
message = _('This field must be unique for the "{date_field}" year.')
157220

158-
def get_filter_kwargs(self, attrs):
221+
def filter_queryset(self, attrs, queryset):
159222
value = attrs[self.field]
160223
date = attrs[self.date_field]
161224

162225
filter_kwargs = {}
163226
filter_kwargs[self.field_name] = value
164227
filter_kwargs['%s__year' % self.date_field_name] = date.year
165-
return filter_kwargs
228+
return queryset.filter(**filter_kwargs)

tests/test_validators.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ def test_updated_instance_excluded_from_unique_together(self):
134134
'position': 1
135135
}
136136

137+
def test_unique_together_is_required(self):
138+
"""
139+
In a unique together validation, all fields are required.
140+
"""
141+
data = {'position': 2}
142+
serializer = UniquenessTogetherSerializer(data=data, partial=True)
143+
assert not serializer.is_valid()
144+
assert serializer.errors == {
145+
'race_name': ['This field is required.']
146+
}
147+
137148
def test_ignore_excluded_fields(self):
138149
"""
139150
When model fields are not included in a serializer, then uniqueness

0 commit comments

Comments
 (0)