diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 55c1f5fb0..18637f21c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -192,6 +192,17 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m - `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"` Changing the `format` parameters only affects representation values. All formats are accepted by `to_internal_value` +## IPAddressField + +A field that ensures the input is a valid IPv4 or IPv6 string. + +Corresponds to `django.forms.fields.IPAddressField` and `django.forms.fields.GenericIPAddressField`. + +**Signature**: `IPAddressField(protocol='both', unpack_ipv4=False, **options)` + +- `protocol` Limits valid inputs to the specified protocol. Accepted values are 'both' (default), 'IPv4' or 'IPv6'. Matching is case insensitive. +- `unpack_ipv4` Unpacks IPv4 mapped addresses like ::ffff:192.0.2.1. If this option is enabled that address would be unpacked to 192.0.2.1. Default is disabled. Can only be used when protocol is set to 'both'. + --- # Numeric fields diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 000c3905b..d9322ec25 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -2,12 +2,13 @@ from __future__ import unicode_literals from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError as DjangoValidationError -from django.core.validators import RegexValidator +from django.core.validators import RegexValidator, ip_address_validators from django.forms import ImageField as DjangoImageField from django.utils import six, timezone from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.encoding import is_protected_type, smart_text from django.utils.translation import ugettext_lazy as _ +from django.utils.ipv6 import clean_ipv6_address from rest_framework import ISO_8601 from rest_framework.compat import ( EmailValidator, MinValueValidator, MaxValueValidator, @@ -672,6 +673,31 @@ class UUIDField(Field): return getattr(value, self.uuid_format) +class IPAddressField(CharField): + """Support both IPAddressField and GenericIPAddressField""" + + default_error_messages = { + 'invalid': _('Enter a valid IPv4 or IPv6 address.'), + } + + def __init__(self, protocol='both', **kwargs): + self.protocol = protocol.lower() + self.unpack_ipv4 = (self.protocol == 'both') + super(IPAddressField, self).__init__(**kwargs) + validators, error_message = ip_address_validators(protocol, self.unpack_ipv4) + self.validators.extend(validators) + + def to_internal_value(self, data): + if data and ':' in data: + try: + if self.protocol in ('both', 'ipv6'): + return clean_ipv6_address(data, self.unpack_ipv4) + except DjangoValidationError: + self.fail('invalid', value=data) + + return super(IPAddressField, self).to_internal_value(data) + + # Number types... class IntegerField(Field): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b19c28ac0..cf491c72b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -734,6 +734,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, + models.GenericIPAddressField: IPAddressField, } if ModelDurationField is not None: serializer_field_mapping[ModelDurationField] = DurationField @@ -1360,6 +1361,10 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField +# IPAddressField is deprecated in Django +if hasattr(models, 'IPAddressField'): + ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField + if postgres_fields: class CharMappingField(DictField): child = CharField() diff --git a/tests/test_fields.py b/tests/test_fields.py index c2042999e..fd2e14667 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -549,6 +549,60 @@ class TestUUIDField(FieldValues): self._test_format('hex', '0' * 32) +class TestIPAddressField(FieldValues): + """ + Valid and invalid values for `IPAddressField` + """ + valid_inputs = { + '127.0.0.1': '127.0.0.1', + '192.168.33.255': '192.168.33.255', + '2001:0db8:85a3:0042:1000:8a2e:0370:7334': '2001:db8:85a3:42:1000:8a2e:370:7334', + '2001:cdba:0:0:0:0:3257:9652': '2001:cdba::3257:9652', + '2001:cdba::3257:9652': '2001:cdba::3257:9652' + } + invalid_inputs = { + '127001': ['Enter a valid IPv4 or IPv6 address.'], + '127.122.111.2231': ['Enter a valid IPv4 or IPv6 address.'], + '2001:::9652': ['Enter a valid IPv4 or IPv6 address.'], + '2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'], + } + outputs = {} + field = serializers.IPAddressField() + + +class TestIPv4AddressField(FieldValues): + """ + Valid and invalid values for `IPAddressField` + """ + valid_inputs = { + '127.0.0.1': '127.0.0.1', + '192.168.33.255': '192.168.33.255', + } + invalid_inputs = { + '127001': ['Enter a valid IPv4 address.'], + '127.122.111.2231': ['Enter a valid IPv4 address.'], + } + outputs = {} + field = serializers.IPAddressField(protocol='IPv4') + + +class TestIPv6AddressField(FieldValues): + """ + Valid and invalid values for `IPAddressField` + """ + valid_inputs = { + '2001:0db8:85a3:0042:1000:8a2e:0370:7334': '2001:db8:85a3:42:1000:8a2e:370:7334', + '2001:cdba:0:0:0:0:3257:9652': '2001:cdba::3257:9652', + '2001:cdba::3257:9652': '2001:cdba::3257:9652' + } + invalid_inputs = { + '2001:::9652': ['Enter a valid IPv4 or IPv6 address.'], + '2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'], + } + outputs = {} + field = serializers.IPAddressField(protocol='IPv6') + + # Number types... class TestIntegerField(FieldValues):