Skip to content

add IPAddressField, update docs #2618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/api-guide/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,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
Expand Down
27 changes: 26 additions & 1 deletion rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
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,
Expand Down Expand Up @@ -653,6 +654,30 @@ def to_representation(self, value):
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 ':' in data:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong, but this line looks like we assume the user will always use 'both' protocol, but what happen if we can't unpack the ipv4 address to an ipv6 one? In this case ':' won't be present inside the ip string and the whole check will fail or be skipped.

Is this situation already handled by the use of ip_address_validators method? Should we add a particular test case that handle the situation where the user pass (protocol='ipv4', unpack_ipv4=True) ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds right!

try:
return clean_ipv6_address(data, self.unpack_ipv4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be changed to:

data = clean_ipv6_address(data, self.unpack_ipv4)

or we just "clean" the ipv6 address without validating it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a wrong assumption. is_valid_ipv6_address is called from clean_ipv6_address https://github.com/django/django/blob/0ed7d155635da9f79d4dd67e4889087d3673c6da/django/utils/ipv6.py#L35

except DjangoValidationError:
self.fail('invalid', value=data)

return super(IPAddressField, self).to_internal_value(data)


# Number types...

class IntegerField(Field):
Expand Down
5 changes: 5 additions & 0 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
models.GenericIPAddressField: IPAddressField,
}
serializer_related_field = PrimaryKeyRelatedField
serializer_url_field = HyperlinkedIdentityField
Expand Down Expand Up @@ -1347,6 +1348,10 @@ def get_unique_for_date_validators(self):
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()
Expand Down
21 changes: 21 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,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):
Expand Down