From 4d5eee04a0a4e126d19f31ffc7325685331c0f71 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Sat, 28 Feb 2015 10:11:38 +0300 Subject: [PATCH 1/7] add IPAddressField, update docs --- docs/api-guide/fields.md | 13 ++++++++++++- rest_framework/fields.py | 31 ++++++++++++++++++++++++++++++- tests/test_fields.py | 21 +++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 4d7d9eee8..9c60b3d35 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -188,6 +188,17 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m "de305d54-75b4-431b-adb2-eb6b9e546013" +## 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 @@ -524,7 +535,7 @@ As an example, let's create a field that can be used represent the class name of # We pass the object instance onto `to_representation`, # not just the field attribute. return obj - + def to_representation(self, obj): """ Serialize the object's class name. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 71a9f1938..860fd3070 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, @@ -650,6 +651,34 @@ class UUIDField(Field): return str(value) +class IPAddressField(CharField): + """Support both IPAddressField and GenericIPAddressField""" + + default_error_messages = { + 'invalid': _('Enter a valid IPv4 or IPv6 address.'), + } + + def __init__(self, protocol='both', unpack_ipv4=False, **kwargs): + self.protocol = protocol + self.unpack_ipv4 = unpack_ipv4 + super(IPAddressField, self).__init__(**kwargs) + validators, error_message = ip_address_validators(protocol, unpack_ipv4) + self.validators.extend(validators) + + def to_internal_value(self, data): + if data == '' and self.allow_blank: + return '' + data = data.strip() + + if data and ':' in data: + try: + return clean_ipv6_address(data, self.unpack_ipv4) + except DjangoValidationError: + self.fail('invalid', value=data) + + return data + + # Number types... class IntegerField(Field): diff --git a/tests/test_fields.py b/tests/test_fields.py index 6744cf645..f0451d5ea 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -485,6 +485,27 @@ class TestUUIDField(FieldValues): field = serializers.UUIDField() +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() + + # Number types... class TestIntegerField(FieldValues): From 313b3d7c3b8dfdb159e3570c3baade827bd6d687 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Sat, 28 Feb 2015 10:18:47 +0300 Subject: [PATCH 2/7] Update ModelSerializer mappings --- rest_framework/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d76658b03..aea457905 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -725,7 +725,8 @@ class ModelSerializer(Serializer): models.SmallIntegerField: IntegerField, models.TextField: CharField, models.TimeField: TimeField, - models.URLField: URLField + models.URLField: URLField, + models.GenericIPAddressField: IPAddressField, # Note: Some version-specific mappings also defined below. }) _related_class = PrimaryKeyRelatedField @@ -1137,6 +1138,10 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): ModelSerializer._field_mapping[models.UUIDField] = UUIDField +# IPAddressField is deprecated in Django +if hasattr(models, 'IPAddressField'): + ModelSerializer._field_mapping[models.IPAddressField] = IPAddressField + if postgres_fields: class CharMappingField(DictField): child = CharField() From c44376c613333cce36e830eb846b4cdcecabaf6c Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Wed, 4 Mar 2015 14:17:58 +0300 Subject: [PATCH 3/7] remove unnecessary check --- rest_framework/fields.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d0fcd988b..d19231a2f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -669,17 +669,13 @@ class IPAddressField(CharField): self.validators.extend(validators) def to_internal_value(self, data): - if data == '' and self.allow_blank: - return '' - data = data.strip() - if data and ':' in data: try: return clean_ipv6_address(data, self.unpack_ipv4) except DjangoValidationError: self.fail('invalid', value=data) - return data + return super(IPAddressField, self).to_internal_value(data) # Number types... From 1d883e08252e2e9f1f03071a7ceac8f139fcbc89 Mon Sep 17 00:00:00 2001 From: Andrea Grandi Date: Sun, 22 Mar 2015 16:46:16 +0000 Subject: [PATCH 4/7] Add two more tests for IPAddressField, checking the IPv4 and IPv6 protocols separately --- tests/test_fields.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index df25aba81..f6ccfffe0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -539,6 +539,38 @@ class TestIPAddressField(FieldValues): 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): From a0049dd4897577950c3804163aa501d1d08cd369 Mon Sep 17 00:00:00 2001 From: Andrea Grandi Date: Sun, 22 Mar 2015 23:21:09 +0000 Subject: [PATCH 5/7] Add a blank line to make lint happier --- tests/test_fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index f6ccfffe0..2ff74eccc 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -571,6 +571,7 @@ class TestIPv6AddressField(FieldValues): outputs = {} field = serializers.IPAddressField(protocol='IPv6') + # Number types... class TestIntegerField(FieldValues): From 466575bee60d2575dc5fb69c6e19e1fab3fd6803 Mon Sep 17 00:00:00 2001 From: Andrea Grandi Date: Thu, 26 Mar 2015 18:14:53 +0000 Subject: [PATCH 6/7] Lowercase the input --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d19231a2f..1731c5f24 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -662,7 +662,7 @@ class IPAddressField(CharField): } def __init__(self, protocol='both', unpack_ipv4=False, **kwargs): - self.protocol = protocol + self.protocol = protocol.lower() self.unpack_ipv4 = unpack_ipv4 super(IPAddressField, self).__init__(**kwargs) validators, error_message = ip_address_validators(protocol, unpack_ipv4) From d6effbf779d5afe0cf52afdd771621641f3ef05f Mon Sep 17 00:00:00 2001 From: Andrea Grandi Date: Thu, 2 Apr 2015 19:40:17 +0100 Subject: [PATCH 7/7] Remove unpack_ipv4 parameter --- rest_framework/fields.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1731c5f24..af48310c3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -661,17 +661,18 @@ class IPAddressField(CharField): 'invalid': _('Enter a valid IPv4 or IPv6 address.'), } - def __init__(self, protocol='both', unpack_ipv4=False, **kwargs): + def __init__(self, protocol='both', **kwargs): self.protocol = protocol.lower() - self.unpack_ipv4 = unpack_ipv4 + self.unpack_ipv4 = (self.protocol == 'both') super(IPAddressField, self).__init__(**kwargs) - validators, error_message = ip_address_validators(protocol, unpack_ipv4) + 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: - return clean_ipv6_address(data, self.unpack_ipv4) + if self.protocol in ('both', 'ipv6'): + return clean_ipv6_address(data, self.unpack_ipv4) except DjangoValidationError: self.fail('invalid', value=data)