diff --git a/django/core/validators.py b/django/core/validators.py
index 0f6adf8fdad..458f4195e0a 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -5,6 +5,7 @@ import urlparse
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
+from django.utils.ipv6 import is_valid_ipv6_address
# These values, if given to validate(), will trigger the self.required check.
EMPTY_VALUES = (None, '', [], (), {})
@@ -145,6 +146,41 @@ validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of l
ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
validate_ipv4_address = RegexValidator(ipv4_re, _(u'Enter a valid IPv4 address.'), 'invalid')
+def validate_ipv6_address(value):
+ if not is_valid_ipv6_address(value):
+ raise ValidationError(_(u'Enter a valid IPv6 address.'), code='invalid')
+
+def validate_ipv46_address(value):
+ try:
+ validate_ipv4_address(value)
+ except ValidationError:
+ try:
+ validate_ipv6_address(value)
+ except ValidationError:
+ raise ValidationError(_(u'Enter a valid IPv4 or IPv6 address.'), code='invalid')
+
+ip_address_validator_map = {
+ 'both': ([validate_ipv46_address], _('Enter a valid IPv4 or IPv6 address.')),
+ 'ipv4': ([validate_ipv4_address], _('Enter a valid IPv4 address.')),
+ 'ipv6': ([validate_ipv6_address], _('Enter a valid IPv6 address.')),
+}
+
+def ip_address_validators(protocol, unpack_ipv4):
+ """
+ Depending on the given parameters returns the appropriate validators for
+ the GenericIPAddressField.
+
+ This code is here, because it is exactly the same for the model and the form field.
+ """
+ if protocol != 'both' and unpack_ipv4:
+ raise ValueError(
+ "You can only use `unpack_ipv4` if `protocol` is set to 'both'")
+ try:
+ return ip_address_validator_map[protocol.lower()]
+ except KeyError:
+ raise ValueError("The protocol '%s' is unknown. Supported: %s"
+ % (protocol, ip_address_validator_map.keys()))
+
comma_separated_int_list_re = re.compile('^[\d,]+$')
validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid')
diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py
index 8b026a908d1..53bd57effc1 100644
--- a/django/db/backends/mysql/creation.py
+++ b/django/db/backends/mysql/creation.py
@@ -19,6 +19,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'IntegerField': 'integer',
'BigIntegerField': 'bigint',
'IPAddressField': 'char(15)',
+ 'GenericIPAddressField': 'char(39)',
'NullBooleanField': 'bool',
'OneToOneField': 'integer',
'PositiveIntegerField': 'integer UNSIGNED',
diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py
index 29293db20ad..0403e0a145a 100644
--- a/django/db/backends/oracle/creation.py
+++ b/django/db/backends/oracle/creation.py
@@ -27,6 +27,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'IntegerField': 'NUMBER(11)',
'BigIntegerField': 'NUMBER(19)',
'IPAddressField': 'VARCHAR2(15)',
+ 'GenericIPAddressField': 'VARCHAR2(39)',
'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))',
'OneToOneField': 'NUMBER(11)',
'PositiveIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)',
diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py
index 5d4d50b5cf6..bdd817db4c9 100644
--- a/django/db/backends/postgresql_psycopg2/creation.py
+++ b/django/db/backends/postgresql_psycopg2/creation.py
@@ -21,6 +21,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'IntegerField': 'integer',
'BigIntegerField': 'bigint',
'IPAddressField': 'inet',
+ 'GenericIPAddressField': 'inet',
'NullBooleanField': 'boolean',
'OneToOneField': 'integer',
'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)',
diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py
index 9f2c81dee5a..b1dac3f8ca4 100644
--- a/django/db/backends/postgresql_psycopg2/introspection.py
+++ b/django/db/backends/postgresql_psycopg2/introspection.py
@@ -12,6 +12,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
700: 'FloatField',
701: 'FloatField',
869: 'IPAddressField',
+ 869: 'GenericIPAddressField',
1043: 'CharField',
1082: 'DateField',
1083: 'TimeField',
diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py
index f32bd0a75d2..f6fe6a45341 100644
--- a/django/db/backends/sqlite3/creation.py
+++ b/django/db/backends/sqlite3/creation.py
@@ -20,6 +20,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'IntegerField': 'integer',
'BigIntegerField': 'bigint',
'IPAddressField': 'char(15)',
+ 'GenericIPAddressField': 'char(39)',
'NullBooleanField': 'bool',
'OneToOneField': 'integer',
'PositiveIntegerField': 'integer unsigned',
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 63d0c21bc13..4707cb4a4cf 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -17,6 +17,7 @@ from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, force_unicode, smart_str
from django.utils import datetime_safe
+from django.utils.ipv6 import clean_ipv6_address, is_valid_ipv6_address
class NOT_PROVIDED:
pass
@@ -920,7 +921,7 @@ class BigIntegerField(IntegerField):
class IPAddressField(Field):
empty_strings_allowed = False
- description = _("IP address")
+ description = _("IPv4 address")
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 15
Field.__init__(self, *args, **kwargs)
@@ -933,6 +934,41 @@ class IPAddressField(Field):
defaults.update(kwargs)
return super(IPAddressField, self).formfield(**defaults)
+class GenericIPAddressField(Field):
+ empty_strings_allowed = True
+ description = _("IP address")
+
+ def __init__(self, protocol='both', unpack_ipv4=False, *args, **kwargs):
+ self.unpack_ipv4 = unpack_ipv4
+ self.default_validators, invalid_error_message = \
+ validators.ip_address_validators(protocol, unpack_ipv4)
+ self.default_error_messages['invalid'] = invalid_error_message
+ kwargs['max_length'] = 39
+ Field.__init__(self, *args, **kwargs)
+
+ def get_internal_type(self):
+ return "GenericIPAddressField"
+
+ def to_python(self, value):
+ if value and ':' in value:
+ return clean_ipv6_address(value,
+ self.unpack_ipv4, self.error_messages['invalid'])
+ return value
+
+ def get_prep_value(self, value):
+ if value and ':' in value:
+ try:
+ return clean_ipv6_address(value, self.unpack_ipv4)
+ except ValidationError:
+ pass
+ return value
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class': forms.GenericIPAddressField}
+ defaults.update(kwargs)
+ return super(GenericIPAddressField, self).formfield(**defaults)
+
+
class NullBooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 67564218ab7..d7b1bbe5db7 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -18,6 +18,7 @@ from django.core import validators
from django.utils import formats
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode, smart_str, force_unicode
+from django.utils.ipv6 import clean_ipv6_address
# Provide this import for backwards compatibility.
from django.core.validators import EMPTY_VALUES
@@ -34,8 +35,8 @@ __all__ = (
'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField',
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
- 'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
- 'TypedChoiceField', 'TypedMultipleChoiceField'
+ 'SplitDateTimeField', 'IPAddressField', 'GenericIPAddressField', 'FilePathField',
+ 'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField'
)
@@ -953,6 +954,25 @@ class IPAddressField(CharField):
default_validators = [validators.validate_ipv4_address]
+class GenericIPAddressField(CharField):
+ default_error_messages = {}
+
+ def __init__(self, protocol='both', unpack_ipv4=False, *args, **kwargs):
+ self.unpack_ipv4 = unpack_ipv4
+ self.default_validators, invalid_error_message = \
+ validators.ip_address_validators(protocol, unpack_ipv4)
+ self.default_error_messages['invalid'] = invalid_error_message
+ super(GenericIPAddressField, self).__init__(*args, **kwargs)
+
+ def to_python(self, value):
+ if not value:
+ return ''
+ if value and ':' in value:
+ return clean_ipv6_address(value,
+ self.unpack_ipv4, self.error_messages['invalid'])
+ return value
+
+
class SlugField(CharField):
default_error_messages = {
'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py
new file mode 100644
index 00000000000..e80a0e205f9
--- /dev/null
+++ b/django/utils/ipv6.py
@@ -0,0 +1,267 @@
+# This code was mostly based on ipaddr-py
+# Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/
+# Licensed under the Apache License, Version 2.0 (the "License").
+from django.core.exceptions import ValidationError
+
+def clean_ipv6_address(ip_str, unpack_ipv4=False,
+ error_message="This is not a valid IPv6 address"):
+ """
+ Cleans a IPv6 address string.
+
+ Validity is checked by calling is_valid_ipv6_address() - if an
+ invalid address is passed, ValidationError is raised.
+
+ Replaces the longest continious zero-sequence with "::" and
+ removes leading zeroes and makes sure all hextets are lowercase.
+
+ Args:
+ ip_str: A valid IPv6 address.
+ unpack_ipv4: if an IPv4-mapped address is found,
+ return the plain IPv4 address (default=False).
+ error_message: A error message for in the ValidationError.
+
+ Returns:
+ A compressed IPv6 address, or the same value
+
+ """
+ best_doublecolon_start = -1
+ best_doublecolon_len = 0
+ doublecolon_start = -1
+ doublecolon_len = 0
+
+ if not is_valid_ipv6_address(ip_str):
+ raise ValidationError(error_message)
+
+ # This algorithm can only handle fully exploded
+ # IP strings
+ ip_str = _explode_shorthand_ip_string(ip_str)
+
+ ip_str = _sanitize_ipv4_mapping(ip_str)
+
+ # If needed, unpack the IPv4 and return straight away
+ # - no need in running the rest of the algorithm
+ if unpack_ipv4:
+ ipv4_unpacked = _unpack_ipv4(ip_str)
+
+ if ipv4_unpacked:
+ return ipv4_unpacked
+
+ hextets = ip_str.split(":")
+
+ for index in range(len(hextets)):
+ # Remove leading zeroes
+ hextets[index] = hextets[index].lstrip('0')
+ if not hextets[index]:
+ hextets[index] = '0'
+
+ # Determine best hextet to compress
+ if hextets[index] == '0':
+ doublecolon_len += 1
+ if doublecolon_start == -1:
+ # Start of a sequence of zeros.
+ doublecolon_start = index
+ if doublecolon_len > best_doublecolon_len:
+ # This is the longest sequence of zeros so far.
+ best_doublecolon_len = doublecolon_len
+ best_doublecolon_start = doublecolon_start
+ else:
+ doublecolon_len = 0
+ doublecolon_start = -1
+
+ # Compress the most suitable hextet
+ if best_doublecolon_len > 1:
+ best_doublecolon_end = (best_doublecolon_start +
+ best_doublecolon_len)
+ # For zeros at the end of the address.
+ if best_doublecolon_end == len(hextets):
+ hextets += ['']
+ hextets[best_doublecolon_start:best_doublecolon_end] = ['']
+ # For zeros at the beginning of the address.
+ if best_doublecolon_start == 0:
+ hextets = [''] + hextets
+
+ result = ":".join(hextets)
+
+ return result.lower()
+
+
+def _sanitize_ipv4_mapping(ip_str):
+ """
+ Sanitize IPv4 mapping in a expanded IPv6 address.
+
+ This converts ::ffff:0a0a:0a0a to ::ffff:10.10.10.10.
+ If there is nothing to sanitize, returns an unchanged
+ string.
+
+ Args:
+ ip_str: A string, the expanded IPv6 address.
+
+ Returns:
+ The sanitized output string, if applicable.
+ """
+ if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'):
+ # not an ipv4 mapping
+ return ip_str
+
+ hextets = ip_str.split(':')
+
+ if '.' in hextets[-1]:
+ # already sanitized
+ return ip_str
+
+ ipv4_address = "%d.%d.%d.%d" % (
+ int(hextets[6][0:2], 16),
+ int(hextets[6][2:4], 16),
+ int(hextets[7][0:2], 16),
+ int(hextets[7][2:4], 16),
+ )
+
+ result = ':'.join(hextets[0:6])
+ result += ':' + ipv4_address
+
+ return result
+
+def _unpack_ipv4(ip_str):
+ """
+ Unpack an IPv4 address that was mapped in a compressed IPv6 address.
+
+ This converts 0000:0000:0000:0000:0000:ffff:10.10.10.10 to 10.10.10.10.
+ If there is nothing to sanitize, returns None.
+
+ Args:
+ ip_str: A string, the expanded IPv6 address.
+
+ Returns:
+ The unpacked IPv4 address, or None if there was nothing to unpack.
+ """
+ if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'):
+ return None
+
+ hextets = ip_str.split(':')
+ return hextets[-1]
+
+def is_valid_ipv6_address(ip_str):
+ """
+ Ensure we have a valid IPv6 address.
+
+ Args:
+ ip_str: A string, the IPv6 address.
+
+ Returns:
+ A boolean, True if this is a valid IPv6 address.
+
+ """
+ from django.core.validators import validate_ipv4_address
+
+ # We need to have at least one ':'.
+ if ':' not in ip_str:
+ return False
+
+ # We can only have one '::' shortener.
+ if ip_str.count('::') > 1:
+ return False
+
+ # '::' should be encompassed by start, digits or end.
+ if ':::' in ip_str:
+ return False
+
+ # A single colon can neither start nor end an address.
+ if ((ip_str.startswith(':') and not ip_str.startswith('::')) or
+ (ip_str.endswith(':') and not ip_str.endswith('::'))):
+ return False
+
+ # We can never have more than 7 ':' (1::2:3:4:5:6:7:8 is invalid)
+ if ip_str.count(':') > 7:
+ return False
+
+ # If we have no concatenation, we need to have 8 fields with 7 ':'.
+ if '::' not in ip_str and ip_str.count(':') != 7:
+ # We might have an IPv4 mapped address.
+ if ip_str.count('.') != 3:
+ return False
+
+ ip_str = _explode_shorthand_ip_string(ip_str)
+
+ # Now that we have that all squared away, let's check that each of the
+ # hextets are between 0x0 and 0xFFFF.
+ for hextet in ip_str.split(':'):
+ if hextet.count('.') == 3:
+ # If we have an IPv4 mapped address, the IPv4 portion has to
+ # be at the end of the IPv6 portion.
+ if not ip_str.split(':')[-1] == hextet:
+ return False
+ try:
+ validate_ipv4_address(hextet)
+ except ValidationError:
+ return False
+ else:
+ try:
+ # a value error here means that we got a bad hextet,
+ # something like 0xzzzz
+ if int(hextet, 16) < 0x0 or int(hextet, 16) > 0xFFFF:
+ return False
+ except ValueError:
+ return False
+ return True
+
+
+def _explode_shorthand_ip_string(ip_str):
+ """
+ Expand a shortened IPv6 address.
+
+ Args:
+ ip_str: A string, the IPv6 address.
+
+ Returns:
+ A string, the expanded IPv6 address.
+
+ """
+ if not _is_shorthand_ip(ip_str):
+ # We've already got a longhand ip_str.
+ return ip_str
+
+ new_ip = []
+ hextet = ip_str.split('::')
+
+ # If there is a ::, we need to expand it with zeroes
+ # to get to 8 hextets - unless there is a dot in the last hextet,
+ # meaning we're doing v4-mapping
+ if '.' in ip_str.split(':')[-1]:
+ fill_to = 7
+ else:
+ fill_to = 8
+
+ if len(hextet) > 1:
+ sep = len(hextet[0].split(':')) + len(hextet[1].split(':'))
+ new_ip = hextet[0].split(':')
+
+ for _ in xrange(fill_to - sep):
+ new_ip.append('0000')
+ new_ip += hextet[1].split(':')
+
+ else:
+ new_ip = ip_str.split(':')
+
+ # Now need to make sure every hextet is 4 lower case characters.
+ # If a hextet is < 4 characters, we've got missing leading 0's.
+ ret_ip = []
+ for hextet in new_ip:
+ ret_ip.append(('0' * (4 - len(hextet)) + hextet).lower())
+ return ':'.join(ret_ip)
+
+
+def _is_shorthand_ip(ip_str):
+ """Determine if the address is shortened.
+
+ Args:
+ ip_str: A string, the IPv6 address.
+
+ Returns:
+ A boolean, True if the address is shortened.
+
+ """
+ if ip_str.count('::') == 1:
+ return True
+ if filter(lambda x: len(x) < 4, ip_str.split(':')):
+ return True
+ return False
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index 96f1be949b8..3fc5b8aabf9 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -622,6 +622,45 @@ Takes two optional arguments for validation:
expression.
* Error message keys: ``required``, ``invalid``
+``GenericIPAddressField``
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: GenericIPAddressField(**kwargs)
+
+.. versionadded:: 1.4
+
+A field containing either an IPv4 or an IPv6 address.
+
+ * Default widget: ``TextInput``
+ * Empty value: ``''`` (an empty string)
+ * Normalizes to: A Unicode object. IPv6 addresses are
+ normalized as described below.
+ * Validates that the given value is a valid IP address.
+ * Error message keys: ``required``, ``invalid``
+
+The IPv6 address normalization follows `RFC4291 section 2.2`_, including using
+the IPv4 format suggested in paragraph 3 of that section, like
+``::ffff:192.0.2.0``. For example, ``2001:0::0:01`` would be normalized to
+``2001::1``, and ``::ffff:0a0a:0a0a`` to ``::ffff:10.10.10.10``. All
+characters are converted to lowercase.
+
+.. _RFC4291 section 2.2: http://tools.ietf.org/html/rfc4291#section-2.2
+
+Takes two optional arguments:
+
+.. attribute:: GenericIPAddressField.protocol
+
+ Limits valid inputs to the specified protocol.
+ Accepted values are ``both`` (default), ``IPv4``
+ or ``IPv6``. Matching is case insensitive.
+
+.. attribute:: GenericIPAddressField.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'``.
+
``MultipleChoiceField``
~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 5c4bbd06bc7..7765f2f8ebb 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -760,6 +760,38 @@ single-line input).
An IP address, in string format (e.g. "192.0.2.30"). The admin represents this
as an ```` (a single-line input).
+``GenericIPAddressField``
+-------------------------
+
+.. class:: GenericIPAddressField([protocols=both, unpack_ipv4=False, **options])
+
+.. versionadded:: 1.4
+
+An IPv4 or IPv6 address, in string format (e.g. ``192.0.2.30`` or
+``2a02:42fe::4``). The admin represents this as an ````
+(a single-line input).
+
+The IPv6 address normalization follows `RFC4291 section 2.2`_, including using
+the IPv4 format suggested in paragraph 3 of that section, like
+``::ffff:192.0.2.0``. For example, ``2001:0::0:01`` would be normalized to
+``2001::1``, and ``::ffff:0a0a:0a0a`` to ``::ffff:10.10.10.10``. All
+characters are converted to lowercase.
+
+.. _RFC4291 section 2.2: http://tools.ietf.org/html/rfc4291#section-2.2
+
+.. attribute:: GenericIPAddressField.protocol
+
+ Limits valid inputs to the specified protocol.
+ Accepted values are ``'both'`` (default), ``'IPv4'``
+ or ``'IPv6'``. Matching is case insensitive.
+
+.. attribute:: GenericIPAddressField.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'``.
+
``NullBooleanField``
--------------------
diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt
index 0bb5abd8199..0f299895c57 100644
--- a/docs/ref/validators.txt
+++ b/docs/ref/validators.txt
@@ -130,6 +130,23 @@ to, or in lieu of custom ``field.clean()`` methods.
A :class:`RegexValidator` instance that ensures a value looks like an IPv4
address.
+``validate_ipv6_address``
+-------------------------
+.. versionadded:: 1.4
+
+.. data:: validate_ipv6_address
+
+ Uses :mod:`django.utils.ipv6` to check the validity of an IPv6 address.
+
+``validate_ipv46_address``
+--------------------------
+.. versionadded:: 1.4
+
+.. data:: validate_ipv46_address
+
+ Uses both ``validate_ipv4_address`` and ``validate_ipv6_address`` to
+ ensure a value is either a valid IPv4 or IPv6 address.
+
``validate_comma_separated_integer_list``
-----------------------------------------
.. data:: validate_comma_separated_integer_list
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index 90f925c02e1..0fbcd34d77a 100644
--- a/docs/releases/1.4.txt
+++ b/docs/releases/1.4.txt
@@ -155,6 +155,15 @@ You may override or customize the default filtering by writing a
:ref:`custom filter`. Learn more on
:ref:`Filtering error reports`.
+Extended IPv6 support
+~~~~~~~~~~~~~~~~~~~~~
+
+The previously added support for IPv6 addresses when using the runserver
+management command in Django 1.3 has now been further extended by adding
+a :class:`~django.db.models.fields.GenericIPAddressField` model field,
+a :class:`~django.forms.fields.GenericIPAddressField` form field and
+the validators :data:`~django.core.validators.validate_ipv46_address` and
+:data:`~django.core.validators.validate_ipv6_address`
Minor features
~~~~~~~~~~~~~~
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 6d418a22b9e..031408a1512 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -83,6 +83,8 @@ the full list of conversions:
``IPAddressField`` ``IPAddressField``
+ ``GenericIPAddressField`` ``GenericIPAddressField``
+
``ManyToManyField`` ``ModelMultipleChoiceField`` (see
below)
diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py
index 923e5a88c5f..f92fc1f4ec8 100644
--- a/tests/modeltests/validation/models.py
+++ b/tests/modeltests/validation/models.py
@@ -81,4 +81,12 @@ class FlexibleDatePost(models.Model):
class UniqueErrorsModel(models.Model):
name = models.CharField(max_length=100, unique=True, error_messages={'unique': u'Custom unique name message.'})
- number = models.IntegerField(unique=True, error_messages={'unique': u'Custom unique number message.'})
\ No newline at end of file
+ number = models.IntegerField(unique=True, error_messages={'unique': u'Custom unique number message.'})
+
+class GenericIPAddressTestModel(models.Model):
+ generic_ip = models.GenericIPAddressField(blank=True, unique=True)
+ v4_ip = models.GenericIPAddressField(blank=True, protocol="ipv4")
+ v6_ip = models.GenericIPAddressField(blank=True, protocol="ipv6")
+
+class GenericIPAddressWithUnpackUniqueTestModel(models.Model):
+ generic_v4unpack_ip = models.GenericIPAddressField(blank=True, unique=True, unpack_ipv4=True)
diff --git a/tests/modeltests/validation/tests.py b/tests/modeltests/validation/tests.py
index 4236d8e7c40..bb60267ff09 100644
--- a/tests/modeltests/validation/tests.py
+++ b/tests/modeltests/validation/tests.py
@@ -2,7 +2,8 @@ from django import forms
from django.test import TestCase
from django.core.exceptions import NON_FIELD_ERRORS
from modeltests.validation import ValidationTestCase
-from modeltests.validation.models import Author, Article, ModelToValidate
+from modeltests.validation.models import (Author, Article, ModelToValidate,
+ GenericIPAddressTestModel, GenericIPAddressWithUnpackUniqueTestModel)
# Import other tests for this package.
from modeltests.validation.validators import TestModelsWithValidators
@@ -77,6 +78,7 @@ class BaseModelValidationTests(ValidationTestCase):
mtv = ModelToValidate(number=10, name='Some Name'*100)
self.assertFailsValidation(mtv.full_clean, ['name',])
+
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
@@ -124,3 +126,58 @@ class ModelFormsTests(TestCase):
article = Article(author_id=self.author.id)
form = ArticleForm(data, instance=article)
self.assertEqual(form.errors.keys(), ['pub_date'])
+
+
+class GenericIPAddressFieldTests(ValidationTestCase):
+
+ def test_correct_generic_ip_passes(self):
+ giptm = GenericIPAddressTestModel(generic_ip="1.2.3.4")
+ self.assertEqual(None, giptm.full_clean())
+ giptm = GenericIPAddressTestModel(generic_ip="2001::2")
+ self.assertEqual(None, giptm.full_clean())
+
+ def test_invalid_generic_ip_raises_error(self):
+ giptm = GenericIPAddressTestModel(generic_ip="294.4.2.1")
+ self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
+ giptm = GenericIPAddressTestModel(generic_ip="1:2")
+ self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
+
+ def test_correct_v4_ip_passes(self):
+ giptm = GenericIPAddressTestModel(v4_ip="1.2.3.4")
+ self.assertEqual(None, giptm.full_clean())
+
+ def test_invalid_v4_ip_raises_error(self):
+ giptm = GenericIPAddressTestModel(v4_ip="294.4.2.1")
+ self.assertFailsValidation(giptm.full_clean, ['v4_ip',])
+ giptm = GenericIPAddressTestModel(v4_ip="2001::2")
+ self.assertFailsValidation(giptm.full_clean, ['v4_ip',])
+
+ def test_correct_v6_ip_passes(self):
+ giptm = GenericIPAddressTestModel(v6_ip="2001::2")
+ self.assertEqual(None, giptm.full_clean())
+
+ def test_invalid_v6_ip_raises_error(self):
+ giptm = GenericIPAddressTestModel(v6_ip="1.2.3.4")
+ self.assertFailsValidation(giptm.full_clean, ['v6_ip',])
+ giptm = GenericIPAddressTestModel(v6_ip="1:2")
+ self.assertFailsValidation(giptm.full_clean, ['v6_ip',])
+
+ def test_v6_uniqueness_detection(self):
+ # These two addresses are the same with different syntax
+ giptm = GenericIPAddressTestModel(generic_ip="2001::1:0:0:0:0:2")
+ giptm.save()
+ giptm = GenericIPAddressTestModel(generic_ip="2001:0:1:2")
+ self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
+
+ def test_v4_unpack_uniqueness_detection(self):
+ # These two are different, because we are not doing IPv4 unpacking
+ giptm = GenericIPAddressTestModel(generic_ip="::ffff:10.10.10.10")
+ giptm.save()
+ giptm = GenericIPAddressTestModel(generic_ip="10.10.10.10")
+ self.assertEqual(None, giptm.full_clean())
+
+ # These two are the same, because we are doing IPv4 unpacking
+ giptm = GenericIPAddressWithUnpackUniqueTestModel(generic_v4unpack_ip="::ffff:18.52.18.52")
+ giptm.save()
+ giptm = GenericIPAddressWithUnpackUniqueTestModel(generic_v4unpack_ip="18.52.18.52")
+ self.assertFailsValidation(giptm.full_clean, ['generic_v4unpack_ip',])
diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py
index cfb7b4b7934..9e254b9b900 100644
--- a/tests/modeltests/validators/tests.py
+++ b/tests/modeltests/validators/tests.py
@@ -52,6 +52,31 @@ TEST_DATA = (
(validate_ipv4_address, '25,1,1,1', ValidationError),
(validate_ipv4_address, '25.1 .1.1', ValidationError),
+ # validate_ipv6_address uses django.utils.ipv6, which
+ # is tested in much greater detail in it's own testcase
+ (validate_ipv6_address, 'fe80::1', None),
+ (validate_ipv6_address, '::1', None),
+ (validate_ipv6_address, '1:2:3:4:5:6:7:8', None),
+
+ (validate_ipv6_address, '1:2', ValidationError),
+ (validate_ipv6_address, '::zzz', ValidationError),
+ (validate_ipv6_address, '12345::', ValidationError),
+
+ (validate_ipv46_address, '1.1.1.1', None),
+ (validate_ipv46_address, '255.0.0.0', None),
+ (validate_ipv46_address, '0.0.0.0', None),
+ (validate_ipv46_address, 'fe80::1', None),
+ (validate_ipv46_address, '::1', None),
+ (validate_ipv46_address, '1:2:3:4:5:6:7:8', None),
+
+ (validate_ipv46_address, '256.1.1.1', ValidationError),
+ (validate_ipv46_address, '25.1.1.', ValidationError),
+ (validate_ipv46_address, '25,1,1,1', ValidationError),
+ (validate_ipv46_address, '25.1 .1.1', ValidationError),
+ (validate_ipv46_address, '1:2', ValidationError),
+ (validate_ipv46_address, '::zzz', ValidationError),
+ (validate_ipv46_address, '12345::', ValidationError),
+
(validate_comma_separated_integer_list, '1', None),
(validate_comma_separated_integer_list, '1,2,3', None),
(validate_comma_separated_integer_list, '1,2,3,', None),
diff --git a/tests/regressiontests/forms/tests/error_messages.py b/tests/regressiontests/forms/tests/error_messages.py
index a9f4f37a8d6..981016f184e 100644
--- a/tests/regressiontests/forms/tests/error_messages.py
+++ b/tests/regressiontests/forms/tests/error_messages.py
@@ -196,6 +196,15 @@ class FormsErrorMessagesTestCase(unittest.TestCase, AssertFormErrorsMixin):
self.assertFormErrors([u'REQUIRED'], f.clean, '')
self.assertFormErrors([u'INVALID IP ADDRESS'], f.clean, '127.0.0')
+ def test_generic_ipaddressfield(self):
+ e = {
+ 'required': 'REQUIRED',
+ 'invalid': 'INVALID IP ADDRESS',
+ }
+ f = GenericIPAddressField(error_messages=e)
+ self.assertFormErrors([u'REQUIRED'], f.clean, '')
+ self.assertFormErrors([u'INVALID IP ADDRESS'], f.clean, '127.0.0')
+
def test_subclassing_errorlist(self):
class TestForm(Form):
first_name = CharField()
diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py
index 7d209616e89..b8b32cb1515 100644
--- a/tests/regressiontests/forms/tests/extra.py
+++ b/tests/regressiontests/forms/tests/extra.py
@@ -460,6 +460,86 @@ class FormsExtraTestCase(unittest.TestCase, AssertFormErrorsMixin):
self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5')
self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '256.125.1.5')
+ def test_generic_ipaddress_invalid_arguments(self):
+ self.assertRaises(ValueError, GenericIPAddressField, protocol="hamster")
+ self.assertRaises(ValueError, GenericIPAddressField, protocol="ipv4", unpack_ipv4=True)
+
+ def test_generic_ipaddress_as_generic(self):
+ # The edge cases of the IPv6 validation code are not deeply tested
+ # here, they are covered in the tests for django.utils.ipv6
+ f = GenericIPAddressField()
+ self.assertFormErrors([u'This field is required.'], f.clean, '')
+ self.assertFormErrors([u'This field is required.'], f.clean, None)
+ self.assertEqual(f.clean('127.0.0.1'), u'127.0.0.1')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, 'foo')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '127.0.0.')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1.2.3.4.5')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '256.125.1.5')
+ self.assertEqual(f.clean('fe80::223:6cff:fe8a:2e8a'), u'fe80::223:6cff:fe8a:2e8a')
+ self.assertEqual(f.clean('2a02::223:6cff:fe8a:2e8a'), u'2a02::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '12345:2:3:4')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3::4')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1:2')
+
+ def test_generic_ipaddress_as_ipv4_only(self):
+ f = GenericIPAddressField(protocol="IPv4")
+ self.assertFormErrors([u'This field is required.'], f.clean, '')
+ self.assertFormErrors([u'This field is required.'], f.clean, None)
+ self.assertEqual(f.clean('127.0.0.1'), u'127.0.0.1')
+ self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, 'foo')
+ self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '127.0.0.')
+ self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5')
+ self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '256.125.1.5')
+ self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, 'fe80::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '2a02::223:6cff:fe8a:2e8a')
+
+ def test_generic_ipaddress_as_ipv4_only(self):
+ f = GenericIPAddressField(protocol="IPv6")
+ self.assertFormErrors([u'This field is required.'], f.clean, '')
+ self.assertFormErrors([u'This field is required.'], f.clean, None)
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '127.0.0.1')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, 'foo')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '127.0.0.')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1.2.3.4.5')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '256.125.1.5')
+ self.assertEqual(f.clean('fe80::223:6cff:fe8a:2e8a'), u'fe80::223:6cff:fe8a:2e8a')
+ self.assertEqual(f.clean('2a02::223:6cff:fe8a:2e8a'), u'2a02::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '12345:2:3:4')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1::2:3::4')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
+ self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2')
+
+ def test_generic_ipaddress_as_generic_not_required(self):
+ f = GenericIPAddressField(required=False)
+ self.assertEqual(f.clean(''), u'')
+ self.assertEqual(f.clean(None), u'')
+ self.assertEqual(f.clean('127.0.0.1'), u'127.0.0.1')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, 'foo')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '127.0.0.')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1.2.3.4.5')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '256.125.1.5')
+ self.assertEqual(f.clean('fe80::223:6cff:fe8a:2e8a'), u'fe80::223:6cff:fe8a:2e8a')
+ self.assertEqual(f.clean('2a02::223:6cff:fe8a:2e8a'), u'2a02::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '12345:2:3:4')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3::4')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, 'foo::223:6cff:fe8a:2e8a')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1::2:3:4:5:6:7:8')
+ self.assertFormErrors([u'Enter a valid IPv4 or IPv6 address.'], f.clean, '1:2')
+
+ def test_generic_ipaddress_normalization(self):
+ # Test the normalising code
+ f = GenericIPAddressField()
+ self.assertEqual(f.clean('::ffff:0a0a:0a0a'), u'::ffff:10.10.10.10')
+ self.assertEqual(f.clean('::ffff:10.10.10.10'), u'::ffff:10.10.10.10')
+ self.assertEqual(f.clean('2001:000:a:0000:0:fe:fe:beef'), u'2001:0:a::fe:fe:beef')
+ self.assertEqual(f.clean('2001::a:0000:0:fe:fe:beef'), u'2001:0:a::fe:fe:beef')
+
+ f = GenericIPAddressField(unpack_ipv4=True)
+ self.assertEqual(f.clean('::ffff:0a0a:0a0a'), u'10.10.10.10')
+
def test_smart_unicode(self):
class Test:
def __str__(self):
diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py
index 3a2c81acefd..b3ae1feaa56 100644
--- a/tests/regressiontests/serializers_regress/models.py
+++ b/tests/regressiontests/serializers_regress/models.py
@@ -52,6 +52,9 @@ class BigIntegerData(models.Model):
class IPAddressData(models.Model):
data = models.IPAddressField(null=True)
+class GenericIPAddressData(models.Model):
+ data = models.GenericIPAddressField(null=True)
+
class NullBooleanData(models.Model):
data = models.NullBooleanField(null=True)
@@ -187,6 +190,9 @@ class IntegerPKData(models.Model):
class IPAddressPKData(models.Model):
data = models.IPAddressField(primary_key=True)
+class GenericIPAddressPKData(models.Model):
+ data = models.GenericIPAddressField(primary_key=True)
+
# This is just a Boolean field with null=True, and we can't test a PK value of NULL.
# class NullBooleanPKData(models.Model):
# data = models.NullBooleanField(primary_key=True)
diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py
index 97b2a792eb4..90a438c43a7 100644
--- a/tests/regressiontests/serializers_regress/tests.py
+++ b/tests/regressiontests/serializers_regress/tests.py
@@ -196,6 +196,8 @@ test_data = [
#(XX, ImageData
(data_obj, 90, IPAddressData, "127.0.0.1"),
(data_obj, 91, IPAddressData, None),
+ (data_obj, 95, GenericIPAddressData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
+ (data_obj, 96, GenericIPAddressData, None),
(data_obj, 100, NullBooleanData, True),
(data_obj, 101, NullBooleanData, False),
(data_obj, 102, NullBooleanData, None),
@@ -298,6 +300,7 @@ The end."""),
(pk_obj, 682, IntegerPKData, 0),
# (XX, ImagePKData
(pk_obj, 690, IPAddressPKData, "127.0.0.1"),
+ (pk_obj, 695, GenericIPAddressPKData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
# (pk_obj, 700, NullBooleanPKData, True),
# (pk_obj, 701, NullBooleanPKData, False),
(pk_obj, 710, PhonePKData, "212-634-5789"),
diff --git a/tests/regressiontests/utils/ipv6.py b/tests/regressiontests/utils/ipv6.py
new file mode 100644
index 00000000000..86d1ad1573f
--- /dev/null
+++ b/tests/regressiontests/utils/ipv6.py
@@ -0,0 +1,51 @@
+from django.utils import unittest
+from django.utils.ipv6 import is_valid_ipv6_address, clean_ipv6_address
+
+class TestUtilsIPv6(unittest.TestCase):
+
+ def test_validates_correct_plain_address(self):
+ self.assertTrue(is_valid_ipv6_address('fe80::223:6cff:fe8a:2e8a'))
+ self.assertTrue(is_valid_ipv6_address('2a02::223:6cff:fe8a:2e8a'))
+ self.assertTrue(is_valid_ipv6_address('1::2:3:4:5:6:7'))
+ self.assertTrue(is_valid_ipv6_address('::'))
+ self.assertTrue(is_valid_ipv6_address('::a'))
+ self.assertTrue(is_valid_ipv6_address('2::'))
+
+ def test_validates_correct_with_v4mapping(self):
+ self.assertTrue(is_valid_ipv6_address('::ffff:254.42.16.14'))
+ self.assertTrue(is_valid_ipv6_address('::ffff:0a0a:0a0a'))
+
+ def test_validates_incorrect_plain_address(self):
+ self.assertFalse(is_valid_ipv6_address('foo'))
+ self.assertFalse(is_valid_ipv6_address('127.0.0.1'))
+ self.assertFalse(is_valid_ipv6_address('12345::'))
+ self.assertFalse(is_valid_ipv6_address('1::2:3::4'))
+ self.assertFalse(is_valid_ipv6_address('1::zzz'))
+ self.assertFalse(is_valid_ipv6_address('1::2:3:4:5:6:7:8'))
+ self.assertFalse(is_valid_ipv6_address('1:2'))
+ self.assertFalse(is_valid_ipv6_address('1:::2'))
+
+ def test_validates_incorrect_with_v4mapping(self):
+ self.assertFalse(is_valid_ipv6_address('::ffff:999.42.16.14'))
+ self.assertFalse(is_valid_ipv6_address('::ffff:zzzz:0a0a'))
+ # The ::1.2.3.4 format used to be valid but was deprecated
+ # in rfc4291 section 2.5.5.1
+ self.assertTrue(is_valid_ipv6_address('::254.42.16.14'))
+ self.assertTrue(is_valid_ipv6_address('::0a0a:0a0a'))
+ self.assertFalse(is_valid_ipv6_address('::999.42.16.14'))
+ self.assertFalse(is_valid_ipv6_address('::zzzz:0a0a'))
+
+ def test_cleanes_plain_address(self):
+ self.assertEqual(clean_ipv6_address('DEAD::0:BEEF'), u'dead::beef')
+ self.assertEqual(clean_ipv6_address('2001:000:a:0000:0:fe:fe:beef'), u'2001:0:a::fe:fe:beef')
+ self.assertEqual(clean_ipv6_address('2001::a:0000:0:fe:fe:beef'), u'2001:0:a::fe:fe:beef')
+
+ def test_cleanes_with_v4_mapping(self):
+ self.assertEqual(clean_ipv6_address('::ffff:0a0a:0a0a'), u'::ffff:10.10.10.10')
+ self.assertEqual(clean_ipv6_address('::ffff:1234:1234'), u'::ffff:18.52.18.52')
+ self.assertEqual(clean_ipv6_address('::ffff:18.52.18.52'), u'::ffff:18.52.18.52')
+
+ def test_unpacks_ipv4(self):
+ self.assertEqual(clean_ipv6_address('::ffff:0a0a:0a0a', unpack_ipv4=True), u'10.10.10.10')
+ self.assertEqual(clean_ipv6_address('::ffff:1234:1234', unpack_ipv4=True), u'18.52.18.52')
+ self.assertEqual(clean_ipv6_address('::ffff:18.52.18.52', unpack_ipv4=True), u'18.52.18.52')
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
index 384d3948796..e91adc94e5d 100644
--- a/tests/regressiontests/utils/tests.py
+++ b/tests/regressiontests/utils/tests.py
@@ -19,3 +19,4 @@ from tzinfo import *
from datetime_safe import *
from baseconv import *
from jslex import *
+from ipv6 import *