diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index d35caca0c7..fd456a08c9 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -204,11 +204,17 @@ def get_field_kwargs(field_name, model_field): if validator is not validators.validate_slug ] - # IPAddressField do not need to include the 'validate_ipv46_address' argument, + # IPAddressField does not need to include the IP address validators, + # as it adds its own based on the 'protocol' argument. if isinstance(model_field, models.GenericIPAddressField): + kwargs['protocol'] = model_field.protocol validator_kwarg = [ validator for validator in validator_kwarg - if validator is not validators.validate_ipv46_address + if validator not in ( + validators.validate_ipv46_address, + validators.validate_ipv4_address, + validators.validate_ipv6_address, + ) ] # Our decimal validation is handled in the field code, not validator code. if isinstance(model_field, models.DecimalField): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 0e291a2de8..914b932d73 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -16,7 +16,8 @@ from django.core.exceptions import ImproperlyConfigured from django.core.serializers.json import DjangoJSONEncoder from django.core.validators import ( - MaxValueValidator, MinLengthValidator, MinValueValidator + MaxLengthValidator, MaxValueValidator, MinLengthValidator, + MinValueValidator ) from django.db import models from django.db.models.signals import m2m_changed @@ -435,9 +436,90 @@ class Meta: s = TestSerializer(data={'address': 'not an ip address'}) self.assertFalse(s.is_valid()) - self.assertEqual(1, len(s.errors['address']), - 'Unexpected number of validation errors: ' - '{}'.format(s.errors)) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv4 or IPv6 address.']) + + def test_ip_address_validation_with_custom_validator(self): + class IPAddressFieldModel(models.Model): + address = models.GenericIPAddressField( + # MaxLengthValidator is an unhashable type + validators=[MaxLengthValidator(15)], + ) + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPAddressFieldModel + fields = '__all__' + + s = TestSerializer(data={'address': 'not an ip address'}) + self.assertFalse(s.is_valid()) + self.assertEqual( + s.errors['address'], + [ + 'Ensure this value has at most 15 characters (it has 17).', + 'Enter a valid IPv4 or IPv6 address.', + ], + ) + + def test_ip_address_validation_with_protocol_ipv4(self): + class IPv4AddressFieldModel(models.Model): + address = models.GenericIPAddressField(protocol='IPv4') + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPv4AddressFieldModel + fields = '__all__' + + expected = dedent(""" + TestSerializer(): + id = IntegerField(label='ID', read_only=True) + address = IPAddressField(protocol='IPv4') + """) + self.assertEqual(repr(TestSerializer()), expected) + + s = TestSerializer(data={'address': 'not an ip address'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv4 address.']) + + # An IPv6 address is not valid for an IPv4-only field. + s = TestSerializer(data={'address': '2001:db8::1'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv4 address.']) + + s = TestSerializer(data={'address': '192.0.2.1'}) + self.assertTrue(s.is_valid(), s.errors) + + def test_ip_address_validation_with_protocol_ipv6(self): + class IPv6AddressFieldModel(models.Model): + address = models.GenericIPAddressField(protocol='IPv6') + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = IPv6AddressFieldModel + fields = '__all__' + + expected = dedent(""" + TestSerializer(): + id = IntegerField(label='ID', read_only=True) + address = IPAddressField(protocol='IPv6') + """) + self.assertEqual(repr(TestSerializer()), expected) + + s = TestSerializer(data={'address': 'not an ip address'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv6 address.']) + + # An IPv4 address is not valid for an IPv6-only field. + s = TestSerializer(data={'address': '192.0.2.1'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors['address'], + ['Enter a valid IPv6 address.']) + + s = TestSerializer(data={'address': '2001:db8::1'}) + self.assertTrue(s.is_valid(), s.errors) @pytest.mark.skipif('not postgres_fields')