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:
parent
87571cdb37
commit
ce3c281090
|
@ -5,6 +5,7 @@ import urlparse
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_unicode
|
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.
|
# These values, if given to validate(), will trigger the self.required check.
|
||||||
EMPTY_VALUES = (None, '', [], (), {})
|
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}$')
|
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')
|
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,]+$')
|
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')
|
validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid')
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'IntegerField': 'integer',
|
'IntegerField': 'integer',
|
||||||
'BigIntegerField': 'bigint',
|
'BigIntegerField': 'bigint',
|
||||||
'IPAddressField': 'char(15)',
|
'IPAddressField': 'char(15)',
|
||||||
|
'GenericIPAddressField': 'char(39)',
|
||||||
'NullBooleanField': 'bool',
|
'NullBooleanField': 'bool',
|
||||||
'OneToOneField': 'integer',
|
'OneToOneField': 'integer',
|
||||||
'PositiveIntegerField': 'integer UNSIGNED',
|
'PositiveIntegerField': 'integer UNSIGNED',
|
||||||
|
|
|
@ -27,6 +27,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'IntegerField': 'NUMBER(11)',
|
'IntegerField': 'NUMBER(11)',
|
||||||
'BigIntegerField': 'NUMBER(19)',
|
'BigIntegerField': 'NUMBER(19)',
|
||||||
'IPAddressField': 'VARCHAR2(15)',
|
'IPAddressField': 'VARCHAR2(15)',
|
||||||
|
'GenericIPAddressField': 'VARCHAR2(39)',
|
||||||
'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))',
|
'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))',
|
||||||
'OneToOneField': 'NUMBER(11)',
|
'OneToOneField': 'NUMBER(11)',
|
||||||
'PositiveIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)',
|
'PositiveIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)',
|
||||||
|
|
|
@ -21,6 +21,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'IntegerField': 'integer',
|
'IntegerField': 'integer',
|
||||||
'BigIntegerField': 'bigint',
|
'BigIntegerField': 'bigint',
|
||||||
'IPAddressField': 'inet',
|
'IPAddressField': 'inet',
|
||||||
|
'GenericIPAddressField': 'inet',
|
||||||
'NullBooleanField': 'boolean',
|
'NullBooleanField': 'boolean',
|
||||||
'OneToOneField': 'integer',
|
'OneToOneField': 'integer',
|
||||||
'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)',
|
'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)',
|
||||||
|
|
|
@ -12,6 +12,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
700: 'FloatField',
|
700: 'FloatField',
|
||||||
701: 'FloatField',
|
701: 'FloatField',
|
||||||
869: 'IPAddressField',
|
869: 'IPAddressField',
|
||||||
|
869: 'GenericIPAddressField',
|
||||||
1043: 'CharField',
|
1043: 'CharField',
|
||||||
1082: 'DateField',
|
1082: 'DateField',
|
||||||
1083: 'TimeField',
|
1083: 'TimeField',
|
||||||
|
|
|
@ -20,6 +20,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'IntegerField': 'integer',
|
'IntegerField': 'integer',
|
||||||
'BigIntegerField': 'bigint',
|
'BigIntegerField': 'bigint',
|
||||||
'IPAddressField': 'char(15)',
|
'IPAddressField': 'char(15)',
|
||||||
|
'GenericIPAddressField': 'char(39)',
|
||||||
'NullBooleanField': 'bool',
|
'NullBooleanField': 'bool',
|
||||||
'OneToOneField': 'integer',
|
'OneToOneField': 'integer',
|
||||||
'PositiveIntegerField': 'integer unsigned',
|
'PositiveIntegerField': 'integer unsigned',
|
||||||
|
|
|
@ -17,6 +17,7 @@ from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_unicode, force_unicode, smart_str
|
from django.utils.encoding import smart_unicode, force_unicode, smart_str
|
||||||
from django.utils import datetime_safe
|
from django.utils import datetime_safe
|
||||||
|
from django.utils.ipv6 import clean_ipv6_address, is_valid_ipv6_address
|
||||||
|
|
||||||
class NOT_PROVIDED:
|
class NOT_PROVIDED:
|
||||||
pass
|
pass
|
||||||
|
@ -920,7 +921,7 @@ class BigIntegerField(IntegerField):
|
||||||
|
|
||||||
class IPAddressField(Field):
|
class IPAddressField(Field):
|
||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
description = _("IP address")
|
description = _("IPv4 address")
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['max_length'] = 15
|
kwargs['max_length'] = 15
|
||||||
Field.__init__(self, *args, **kwargs)
|
Field.__init__(self, *args, **kwargs)
|
||||||
|
@ -933,6 +934,41 @@ class IPAddressField(Field):
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return super(IPAddressField, self).formfield(**defaults)
|
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):
|
class NullBooleanField(Field):
|
||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
|
|
|
@ -18,6 +18,7 @@ from django.core import validators
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_unicode, smart_str, force_unicode
|
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.
|
# Provide this import for backwards compatibility.
|
||||||
from django.core.validators import EMPTY_VALUES
|
from django.core.validators import EMPTY_VALUES
|
||||||
|
@ -34,8 +35,8 @@ __all__ = (
|
||||||
'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField',
|
'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField',
|
||||||
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
|
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
|
||||||
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
|
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
|
||||||
'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
|
'SplitDateTimeField', 'IPAddressField', 'GenericIPAddressField', 'FilePathField',
|
||||||
'TypedChoiceField', 'TypedMultipleChoiceField'
|
'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -953,6 +954,25 @@ class IPAddressField(CharField):
|
||||||
default_validators = [validators.validate_ipv4_address]
|
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):
|
class SlugField(CharField):
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
|
'invalid': _(u"Enter a valid 'slug' consisting of letters, numbers,"
|
||||||
|
|
|
@ -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
|
|
@ -622,6 +622,45 @@ Takes two optional arguments for validation:
|
||||||
expression.
|
expression.
|
||||||
* Error message keys: ``required``, ``invalid``
|
* 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``
|
``MultipleChoiceField``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -760,6 +760,38 @@ single-line input).
|
||||||
An IP address, in string format (e.g. "192.0.2.30"). The admin represents this
|
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).
|
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``
|
``NullBooleanField``
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|
|
@ -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
|
A :class:`RegexValidator` instance that ensures a value looks like an IPv4
|
||||||
address.
|
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``
|
``validate_comma_separated_integer_list``
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
.. data:: validate_comma_separated_integer_list
|
.. data:: validate_comma_separated_integer_list
|
||||||
|
|
|
@ -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:`custom filter<custom-error-reports>`. Learn more on
|
||||||
:ref:`Filtering error reports<filtering-error-reports>`.
|
: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
|
Minor features
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -83,6 +83,8 @@ the full list of conversions:
|
||||||
|
|
||||||
``IPAddressField`` ``IPAddressField``
|
``IPAddressField`` ``IPAddressField``
|
||||||
|
|
||||||
|
``GenericIPAddressField`` ``GenericIPAddressField``
|
||||||
|
|
||||||
``ManyToManyField`` ``ModelMultipleChoiceField`` (see
|
``ManyToManyField`` ``ModelMultipleChoiceField`` (see
|
||||||
below)
|
below)
|
||||||
|
|
||||||
|
|
|
@ -81,4 +81,12 @@ class FlexibleDatePost(models.Model):
|
||||||
|
|
||||||
class UniqueErrorsModel(models.Model):
|
class UniqueErrorsModel(models.Model):
|
||||||
name = models.CharField(max_length=100, unique=True, error_messages={'unique': u'Custom unique name message.'})
|
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)
|
||||||
|
|
|
@ -2,7 +2,8 @@ from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.exceptions import NON_FIELD_ERRORS
|
from django.core.exceptions import NON_FIELD_ERRORS
|
||||||
from modeltests.validation import ValidationTestCase
|
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.
|
# Import other tests for this package.
|
||||||
from modeltests.validation.validators import TestModelsWithValidators
|
from modeltests.validation.validators import TestModelsWithValidators
|
||||||
|
@ -77,6 +78,7 @@ class BaseModelValidationTests(ValidationTestCase):
|
||||||
mtv = ModelToValidate(number=10, name='Some Name'*100)
|
mtv = ModelToValidate(number=10, name='Some Name'*100)
|
||||||
self.assertFailsValidation(mtv.full_clean, ['name',])
|
self.assertFailsValidation(mtv.full_clean, ['name',])
|
||||||
|
|
||||||
|
|
||||||
class ArticleForm(forms.ModelForm):
|
class ArticleForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Article
|
model = Article
|
||||||
|
@ -124,3 +126,58 @@ class ModelFormsTests(TestCase):
|
||||||
article = Article(author_id=self.author.id)
|
article = Article(author_id=self.author.id)
|
||||||
form = ArticleForm(data, instance=article)
|
form = ArticleForm(data, instance=article)
|
||||||
self.assertEqual(form.errors.keys(), ['pub_date'])
|
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',])
|
||||||
|
|
|
@ -52,6 +52,31 @@ TEST_DATA = (
|
||||||
(validate_ipv4_address, '25,1,1,1', ValidationError),
|
(validate_ipv4_address, '25,1,1,1', ValidationError),
|
||||||
(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', None),
|
||||||
(validate_comma_separated_integer_list, '1,2,3', None),
|
(validate_comma_separated_integer_list, '1,2,3', None),
|
||||||
(validate_comma_separated_integer_list, '1,2,3,', None),
|
(validate_comma_separated_integer_list, '1,2,3,', None),
|
||||||
|
|
|
@ -196,6 +196,15 @@ class FormsErrorMessagesTestCase(unittest.TestCase, AssertFormErrorsMixin):
|
||||||
self.assertFormErrors([u'REQUIRED'], f.clean, '')
|
self.assertFormErrors([u'REQUIRED'], f.clean, '')
|
||||||
self.assertFormErrors([u'INVALID IP ADDRESS'], f.clean, '127.0.0')
|
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):
|
def test_subclassing_errorlist(self):
|
||||||
class TestForm(Form):
|
class TestForm(Form):
|
||||||
first_name = CharField()
|
first_name = CharField()
|
||||||
|
|
|
@ -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, '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, '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):
|
def test_smart_unicode(self):
|
||||||
class Test:
|
class Test:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -52,6 +52,9 @@ class BigIntegerData(models.Model):
|
||||||
class IPAddressData(models.Model):
|
class IPAddressData(models.Model):
|
||||||
data = models.IPAddressField(null=True)
|
data = models.IPAddressField(null=True)
|
||||||
|
|
||||||
|
class GenericIPAddressData(models.Model):
|
||||||
|
data = models.GenericIPAddressField(null=True)
|
||||||
|
|
||||||
class NullBooleanData(models.Model):
|
class NullBooleanData(models.Model):
|
||||||
data = models.NullBooleanField(null=True)
|
data = models.NullBooleanField(null=True)
|
||||||
|
|
||||||
|
@ -187,6 +190,9 @@ class IntegerPKData(models.Model):
|
||||||
class IPAddressPKData(models.Model):
|
class IPAddressPKData(models.Model):
|
||||||
data = models.IPAddressField(primary_key=True)
|
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.
|
# This is just a Boolean field with null=True, and we can't test a PK value of NULL.
|
||||||
# class NullBooleanPKData(models.Model):
|
# class NullBooleanPKData(models.Model):
|
||||||
# data = models.NullBooleanField(primary_key=True)
|
# data = models.NullBooleanField(primary_key=True)
|
||||||
|
|
|
@ -196,6 +196,8 @@ test_data = [
|
||||||
#(XX, ImageData
|
#(XX, ImageData
|
||||||
(data_obj, 90, IPAddressData, "127.0.0.1"),
|
(data_obj, 90, IPAddressData, "127.0.0.1"),
|
||||||
(data_obj, 91, IPAddressData, None),
|
(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, 100, NullBooleanData, True),
|
||||||
(data_obj, 101, NullBooleanData, False),
|
(data_obj, 101, NullBooleanData, False),
|
||||||
(data_obj, 102, NullBooleanData, None),
|
(data_obj, 102, NullBooleanData, None),
|
||||||
|
@ -298,6 +300,7 @@ The end."""),
|
||||||
(pk_obj, 682, IntegerPKData, 0),
|
(pk_obj, 682, IntegerPKData, 0),
|
||||||
# (XX, ImagePKData
|
# (XX, ImagePKData
|
||||||
(pk_obj, 690, IPAddressPKData, "127.0.0.1"),
|
(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, 700, NullBooleanPKData, True),
|
||||||
# (pk_obj, 701, NullBooleanPKData, False),
|
# (pk_obj, 701, NullBooleanPKData, False),
|
||||||
(pk_obj, 710, PhonePKData, "212-634-5789"),
|
(pk_obj, 710, PhonePKData, "212-634-5789"),
|
||||||
|
|
|
@ -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')
|
|
@ -19,3 +19,4 @@ from tzinfo import *
|
||||||
from datetime_safe import *
|
from datetime_safe import *
|
||||||
from baseconv import *
|
from baseconv import *
|
||||||
from jslex import *
|
from jslex import *
|
||||||
|
from ipv6 import *
|
||||||
|
|
Loading…
Reference in New Issue