Fixed #811 -- Added support for IPv6 to forms and model fields. Many thanks to Erik Romijn.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16366 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-06-11 13:48:24 +00:00
parent 87571cdb37
commit ce3c281090
23 changed files with 708 additions and 5 deletions

View File

@ -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')

View File

@ -19,6 +19,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'IntegerField': 'integer',
'BigIntegerField': 'bigint',
'IPAddressField': 'char(15)',
'GenericIPAddressField': 'char(39)',
'NullBooleanField': 'bool',
'OneToOneField': 'integer',
'PositiveIntegerField': 'integer UNSIGNED',

View File

@ -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)',

View File

@ -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)',

View File

@ -12,6 +12,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
700: 'FloatField',
701: 'FloatField',
869: 'IPAddressField',
869: 'GenericIPAddressField',
1043: 'CharField',
1082: 'DateField',
1083: 'TimeField',

View File

@ -20,6 +20,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'IntegerField': 'integer',
'BigIntegerField': 'bigint',
'IPAddressField': 'char(15)',
'GenericIPAddressField': 'char(39)',
'NullBooleanField': 'bool',
'OneToOneField': 'integer',
'PositiveIntegerField': 'integer unsigned',

View File

@ -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 = {

View File

@ -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,"

267
django/utils/ipv6.py Normal file
View File

@ -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

View File

@ -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``
~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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 ``<input type="text">`` (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 ``<input type="text">``
(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``
--------------------

View File

@ -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

View File

@ -155,6 +155,15 @@ You may override or customize the default filtering by writing a
:ref:`custom filter<custom-error-reports>`. Learn more on
:ref:`Filtering error reports<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
~~~~~~~~~~~~~~

View File

@ -83,6 +83,8 @@ the full list of conversions:
``IPAddressField`` ``IPAddressField``
``GenericIPAddressField`` ``GenericIPAddressField``
``ManyToManyField`` ``ModelMultipleChoiceField`` (see
below)

View File

@ -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.'})
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)

View File

@ -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',])

View File

@ -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),

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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"),

View File

@ -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')

View File

@ -19,3 +19,4 @@ from tzinfo import *
from datetime_safe import *
from baseconv import *
from jslex import *
from ipv6 import *