Skip to content

Commit a3d6601

Browse files
committed
Merge pull request #3008 from linovia/feature/ipaddress-fix
`IPAddressField` improvements
2 parents e8cc948 + dee5fb5 commit a3d6601

File tree

4 files changed

+97
-1
lines changed

4 files changed

+97
-1
lines changed

docs/api-guide/fields.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,17 @@ A field that ensures the input is a valid UUID string. The `to_internal_value` m
192192
- `'urn'` - RFC 4122 URN representation of the UUID: `"urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a"`
193193
Changing the `format` parameters only affects representation values. All formats are accepted by `to_internal_value`
194194

195+
## IPAddressField
196+
197+
A field that ensures the input is a valid IPv4 or IPv6 string.
198+
199+
Corresponds to `django.forms.fields.IPAddressField` and `django.forms.fields.GenericIPAddressField`.
200+
201+
**Signature**: `IPAddressField(protocol='both', unpack_ipv4=False, **options)`
202+
203+
- `protocol` Limits valid inputs to the specified protocol. Accepted values are 'both' (default), 'IPv4' or 'IPv6'. Matching is case insensitive.
204+
- `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'.
205+
195206
---
196207

197208
# Numeric fields

rest_framework/fields.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
from django.conf import settings
33
from django.core.exceptions import ObjectDoesNotExist
44
from django.core.exceptions import ValidationError as DjangoValidationError
5-
from django.core.validators import RegexValidator
5+
from django.core.validators import RegexValidator, ip_address_validators
66
from django.forms import ImageField as DjangoImageField
77
from django.utils import six, timezone
88
from django.utils.dateparse import parse_date, parse_datetime, parse_time
99
from django.utils.encoding import is_protected_type, smart_text
1010
from django.utils.translation import ugettext_lazy as _
11+
from django.utils.ipv6 import clean_ipv6_address
1112
from rest_framework import ISO_8601
1213
from rest_framework.compat import (
1314
EmailValidator, MinValueValidator, MaxValueValidator,
@@ -672,6 +673,31 @@ def to_representation(self, value):
672673
return getattr(value, self.uuid_format)
673674

674675

676+
class IPAddressField(CharField):
677+
"""Support both IPAddressField and GenericIPAddressField"""
678+
679+
default_error_messages = {
680+
'invalid': _('Enter a valid IPv4 or IPv6 address.'),
681+
}
682+
683+
def __init__(self, protocol='both', **kwargs):
684+
self.protocol = protocol.lower()
685+
self.unpack_ipv4 = (self.protocol == 'both')
686+
super(IPAddressField, self).__init__(**kwargs)
687+
validators, error_message = ip_address_validators(protocol, self.unpack_ipv4)
688+
self.validators.extend(validators)
689+
690+
def to_internal_value(self, data):
691+
if data and ':' in data:
692+
try:
693+
if self.protocol in ('both', 'ipv6'):
694+
return clean_ipv6_address(data, self.unpack_ipv4)
695+
except DjangoValidationError:
696+
self.fail('invalid', value=data)
697+
698+
return super(IPAddressField, self).to_internal_value(data)
699+
700+
675701
# Number types...
676702

677703
class IntegerField(Field):

rest_framework/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ class ModelSerializer(Serializer):
734734
models.TextField: CharField,
735735
models.TimeField: TimeField,
736736
models.URLField: URLField,
737+
models.GenericIPAddressField: IPAddressField,
737738
}
738739
if ModelDurationField is not None:
739740
serializer_field_mapping[ModelDurationField] = DurationField
@@ -1360,6 +1361,10 @@ def get_unique_for_date_validators(self):
13601361
if hasattr(models, 'UUIDField'):
13611362
ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField
13621363

1364+
# IPAddressField is deprecated in Django
1365+
if hasattr(models, 'IPAddressField'):
1366+
ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField
1367+
13631368
if postgres_fields:
13641369
class CharMappingField(DictField):
13651370
child = CharField()

tests/test_fields.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,60 @@ def test_formats(self):
549549
self._test_format('hex', '0' * 32)
550550

551551

552+
class TestIPAddressField(FieldValues):
553+
"""
554+
Valid and invalid values for `IPAddressField`
555+
"""
556+
valid_inputs = {
557+
'127.0.0.1': '127.0.0.1',
558+
'192.168.33.255': '192.168.33.255',
559+
'2001:0db8:85a3:0042:1000:8a2e:0370:7334': '2001:db8:85a3:42:1000:8a2e:370:7334',
560+
'2001:cdba:0:0:0:0:3257:9652': '2001:cdba::3257:9652',
561+
'2001:cdba::3257:9652': '2001:cdba::3257:9652'
562+
}
563+
invalid_inputs = {
564+
'127001': ['Enter a valid IPv4 or IPv6 address.'],
565+
'127.122.111.2231': ['Enter a valid IPv4 or IPv6 address.'],
566+
'2001:::9652': ['Enter a valid IPv4 or IPv6 address.'],
567+
'2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'],
568+
}
569+
outputs = {}
570+
field = serializers.IPAddressField()
571+
572+
573+
class TestIPv4AddressField(FieldValues):
574+
"""
575+
Valid and invalid values for `IPAddressField`
576+
"""
577+
valid_inputs = {
578+
'127.0.0.1': '127.0.0.1',
579+
'192.168.33.255': '192.168.33.255',
580+
}
581+
invalid_inputs = {
582+
'127001': ['Enter a valid IPv4 address.'],
583+
'127.122.111.2231': ['Enter a valid IPv4 address.'],
584+
}
585+
outputs = {}
586+
field = serializers.IPAddressField(protocol='IPv4')
587+
588+
589+
class TestIPv6AddressField(FieldValues):
590+
"""
591+
Valid and invalid values for `IPAddressField`
592+
"""
593+
valid_inputs = {
594+
'2001:0db8:85a3:0042:1000:8a2e:0370:7334': '2001:db8:85a3:42:1000:8a2e:370:7334',
595+
'2001:cdba:0:0:0:0:3257:9652': '2001:cdba::3257:9652',
596+
'2001:cdba::3257:9652': '2001:cdba::3257:9652'
597+
}
598+
invalid_inputs = {
599+
'2001:::9652': ['Enter a valid IPv4 or IPv6 address.'],
600+
'2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'],
601+
}
602+
outputs = {}
603+
field = serializers.IPAddressField(protocol='IPv6')
604+
605+
552606
# Number types...
553607

554608
class TestIntegerField(FieldValues):

0 commit comments

Comments
 (0)