Skip to content

Commit a652ebd

Browse files
authored
Merge pull request #5028 from Ekluv/ekluv_5004
fix unique=True validation for ChoiceField
2 parents 20c7a24 + d66304a commit a652ebd

File tree

2 files changed

+85
-67
lines changed

2 files changed

+85
-67
lines changed

rest_framework/utils/field_mapping.py

Lines changed: 63 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,70 @@ def get_field_kwargs(field_name, model_field):
123123
kwargs['allow_folders'] = model_field.allow_folders
124124

125125
if model_field.choices:
126-
# If this model field contains choices, then return early.
127-
# Further keyword arguments are not valid.
128126
kwargs['choices'] = model_field.choices
129-
return kwargs
130-
131-
# Our decimal validation is handled in the field code, not validator code.
132-
# (In Django 1.9+ this differs from previous style)
133-
if isinstance(model_field, models.DecimalField) and DecimalValidator:
134-
validator_kwarg = [
135-
validator for validator in validator_kwarg
136-
if not isinstance(validator, DecimalValidator)
137-
]
127+
else:
128+
# Ensure that max_value is passed explicitly as a keyword arg,
129+
# rather than as a validator.
130+
max_value = next((
131+
validator.limit_value for validator in validator_kwarg
132+
if isinstance(validator, validators.MaxValueValidator)
133+
), None)
134+
if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
135+
kwargs['max_value'] = max_value
136+
validator_kwarg = [
137+
validator for validator in validator_kwarg
138+
if not isinstance(validator, validators.MaxValueValidator)
139+
]
140+
141+
# Ensure that max_value is passed explicitly as a keyword arg,
142+
# rather than as a validator.
143+
min_value = next((
144+
validator.limit_value for validator in validator_kwarg
145+
if isinstance(validator, validators.MinValueValidator)
146+
), None)
147+
if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
148+
kwargs['min_value'] = min_value
149+
validator_kwarg = [
150+
validator for validator in validator_kwarg
151+
if not isinstance(validator, validators.MinValueValidator)
152+
]
153+
154+
# URLField does not need to include the URLValidator argument,
155+
# as it is explicitly added in.
156+
if isinstance(model_field, models.URLField):
157+
validator_kwarg = [
158+
validator for validator in validator_kwarg
159+
if not isinstance(validator, validators.URLValidator)
160+
]
161+
162+
# EmailField does not need to include the validate_email argument,
163+
# as it is explicitly added in.
164+
if isinstance(model_field, models.EmailField):
165+
validator_kwarg = [
166+
validator for validator in validator_kwarg
167+
if validator is not validators.validate_email
168+
]
169+
170+
# SlugField do not need to include the 'validate_slug' argument,
171+
if isinstance(model_field, models.SlugField):
172+
validator_kwarg = [
173+
validator for validator in validator_kwarg
174+
if validator is not validators.validate_slug
175+
]
176+
177+
# IPAddressField do not need to include the 'validate_ipv46_address' argument,
178+
if isinstance(model_field, models.GenericIPAddressField):
179+
validator_kwarg = [
180+
validator for validator in validator_kwarg
181+
if validator is not validators.validate_ipv46_address
182+
]
183+
# Our decimal validation is handled in the field code, not validator code.
184+
# (In Django 1.9+ this differs from previous style)
185+
if isinstance(model_field, models.DecimalField) and DecimalValidator:
186+
validator_kwarg = [
187+
validator for validator in validator_kwarg
188+
if not isinstance(validator, DecimalValidator)
189+
]
138190

139191
# Ensure that max_length is passed explicitly as a keyword arg,
140192
# rather than as a validator.
@@ -160,62 +212,6 @@ def get_field_kwargs(field_name, model_field):
160212
if not isinstance(validator, validators.MinLengthValidator)
161213
]
162214

163-
# Ensure that max_value is passed explicitly as a keyword arg,
164-
# rather than as a validator.
165-
max_value = next((
166-
validator.limit_value for validator in validator_kwarg
167-
if isinstance(validator, validators.MaxValueValidator)
168-
), None)
169-
if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
170-
kwargs['max_value'] = max_value
171-
validator_kwarg = [
172-
validator for validator in validator_kwarg
173-
if not isinstance(validator, validators.MaxValueValidator)
174-
]
175-
176-
# Ensure that max_value is passed explicitly as a keyword arg,
177-
# rather than as a validator.
178-
min_value = next((
179-
validator.limit_value for validator in validator_kwarg
180-
if isinstance(validator, validators.MinValueValidator)
181-
), None)
182-
if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
183-
kwargs['min_value'] = min_value
184-
validator_kwarg = [
185-
validator for validator in validator_kwarg
186-
if not isinstance(validator, validators.MinValueValidator)
187-
]
188-
189-
# URLField does not need to include the URLValidator argument,
190-
# as it is explicitly added in.
191-
if isinstance(model_field, models.URLField):
192-
validator_kwarg = [
193-
validator for validator in validator_kwarg
194-
if not isinstance(validator, validators.URLValidator)
195-
]
196-
197-
# EmailField does not need to include the validate_email argument,
198-
# as it is explicitly added in.
199-
if isinstance(model_field, models.EmailField):
200-
validator_kwarg = [
201-
validator for validator in validator_kwarg
202-
if validator is not validators.validate_email
203-
]
204-
205-
# SlugField do not need to include the 'validate_slug' argument,
206-
if isinstance(model_field, models.SlugField):
207-
validator_kwarg = [
208-
validator for validator in validator_kwarg
209-
if validator is not validators.validate_slug
210-
]
211-
212-
# IPAddressField do not need to include the 'validate_ipv46_address' argument,
213-
if isinstance(model_field, models.GenericIPAddressField):
214-
validator_kwarg = [
215-
validator for validator in validator_kwarg
216-
if validator is not validators.validate_ipv46_address
217-
]
218-
219215
if getattr(model_field, 'unique', False):
220216
unique_error_message = model_field.error_messages.get('unique', None)
221217
if unique_error_message:

tests/test_model_serializer.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ class Issue3674ChildModel(models.Model):
9999
value = models.CharField(primary_key=True, max_length=64)
100100

101101

102+
class UniqueChoiceModel(models.Model):
103+
CHOICES = (
104+
('choice1', 'choice 1'),
105+
('choice2', 'choice 1'),
106+
)
107+
108+
name = models.CharField(max_length=254, unique=True, choices=CHOICES)
109+
110+
102111
class TestModelSerializer(TestCase):
103112
def test_create_method(self):
104113
class TestSerializer(serializers.ModelSerializer):
@@ -1080,3 +1089,16 @@ class Meta:
10801089
with pytest.raises(AssertionError) as cm:
10811090
TestSerializer(obj).fields
10821091
cm.match(r'readonly_fields')
1092+
1093+
1094+
class Test5004UniqueChoiceField(TestCase):
1095+
def test_unique_choice_field(self):
1096+
class TestUniqueChoiceSerializer(serializers.ModelSerializer):
1097+
class Meta:
1098+
model = UniqueChoiceModel
1099+
fields = '__all__'
1100+
1101+
UniqueChoiceModel.objects.create(name='choice1')
1102+
serializer = TestUniqueChoiceSerializer(data={'name': 'choice1'})
1103+
assert not serializer.is_valid()
1104+
assert serializer.errors == {'name': ['unique choice model with this name already exists.']}

0 commit comments

Comments
 (0)