From cbe78fc6b3b8e579a3b6e0c02001ed9ab2739893 Mon Sep 17 00:00:00 2001
From: Jannis Leidel <jannis@leidel.info>
Date: Fri, 6 May 2011 19:28:46 +0200
Subject: [PATCH] Fixed #811 -- Added IP6AddressField.

---
 django/core/validators.py                          |   24 ++++++++-
 django/db/backends/mysql/creation.py               |    1 +
 django/db/backends/oracle/creation.py              |    1 +
 django/db/backends/postgresql_psycopg2/creation.py |    1 +
 django/db/backends/sqlite3/creation.py             |    1 +
 django/db/models/fields/__init__.py                |   18 ++++++-
 django/forms/fields.py                             |    8 +++-
 django/utils/ip.py                                 |   54 ++++++++++++++++++++
 tests/regressiontests/forms/tests/extra.py         |   22 ++++++++
 .../regressiontests/serializers_regress/models.py  |    6 ++
 tests/regressiontests/serializers_regress/tests.py |    2 +
 11 files changed, 135 insertions(+), 3 deletions(-)
 create mode 100644 django/utils/ip.py

diff --git a/django/core/validators.py b/django/core/validators.py
index a40af0c..33517b6 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.ip import ipv6_normalize
 
 # These values, if given to validate(), will trigger the self.required check.
 EMPTY_VALUES = (None, '', [], (), {})
@@ -20,14 +21,17 @@ class RegexValidator(object):
     regex = ''
     message = _(u'Enter a valid value.')
     code = 'invalid'
+    normalizer = None
 
-    def __init__(self, regex=None, message=None, code=None):
+    def __init__(self, regex=None, message=None, code=None, normalizer=None):
         if regex is not None:
             self.regex = regex
         if message is not None:
             self.message = message
         if code is not None:
             self.code = code
+        if normalizer is not None:
+            self.normalizer = normalizer
 
         if isinstance(self.regex, basestring):
             self.regex = re.compile(regex)
@@ -36,6 +40,12 @@ class RegexValidator(object):
         """
         Validates that the input matches the regular expression.
         """
+        if self.normalizer:
+            try:
+                value = self.normalizer(value)
+            except ValueError:
+                raise ValidationError(self.message, code=self.code)
+
         if not self.regex.search(smart_unicode(value)):
             raise ValidationError(self.message, code=self.code)
 
@@ -143,6 +153,18 @@ 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')
 
+ipv6_re = re.compile(r'^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$')
+validate_ipv6_address = RegexValidator(ipv6_re, _(u'Enter a valid IPv6 address.'), 'invalid', ipv6_normalize)
+
+def validate_ip_address(value):
+    try:
+        validate_ipv4_address(value)
+    except ValidationError:
+        try:
+            validate_ipv6_address(value)
+        except ValidationError:
+            raise ValidationError(_(u'Enter a valid IP address.'), code='invalid')
+
 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 8b026a9..5fa71be 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)',
+        'IP6AddressField':  '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 29293db..2ff4c94 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)',
+        'IP6AddressField':             '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 5d4d50b..388f2d9 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',
+        'IP6AddressField':  'inet',
         'NullBooleanField':  'boolean',
         'OneToOneField':     'integer',
         'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)',
diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py
index f32bd0a..8fa42c7 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)',
+        'IP6AddressField':             '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 9037265..c7f020d 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -919,7 +919,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)
@@ -932,6 +932,22 @@ class IPAddressField(Field):
         defaults.update(kwargs)
         return super(IPAddressField, self).formfield(**defaults)
 
+class IP6AddressField(Field):
+    empty_strings_allowed = False
+    description = _("IPv6 address")
+    def __init__(self, *args, **kwargs):
+        kwargs['max_length'] = 39
+        Field.__init__(self, *args, **kwargs)
+
+    def get_internal_type(self):
+        return "IP6AddressField"
+
+    def formfield(self, **kwargs):
+        defaults = {'form_class': forms.IP6AddressField}
+        defaults.update(kwargs)
+        return super(IP6AddressField, 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 a5ea81d..11fd416 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -38,7 +38,7 @@ __all__ = (
     'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
     'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
     'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField',
-    'TypedChoiceField', 'TypedMultipleChoiceField'
+    'TypedChoiceField', 'TypedMultipleChoiceField', 'IP6AddressField',
 )
 
 
@@ -946,6 +946,12 @@ class IPAddressField(CharField):
     }
     default_validators = [validators.validate_ipv4_address]
 
+class IP6AddressField(CharField):
+    default_error_messages = {
+        'invalid': _(u'Enter a valid IPv6 address.'),
+    }
+    default_validators = [validators.validate_ipv6_address]
+
 
 class SlugField(CharField):
     default_error_messages = {
diff --git a/django/utils/ip.py b/django/utils/ip.py
new file mode 100644
index 0000000..5929689
--- /dev/null
+++ b/django/utils/ip.py
@@ -0,0 +1,54 @@
+def ipv6_normalize(addr):
+    """
+    Normalize an IPv6 address to allow easy regexp validation.
+    Mostly checks the length, and gets rid of tricky things
+    like IPv4 mapped addresses and :: shortcuts
+    
+    Outputs a string
+    """
+    # Some basic error checking
+    if addr.count('::') > 2 or ':::' in addr:
+        raise ValueError
+
+    ip = addr.split(':')
+    nbfull = len([elem for elem in ip if elem != ''])
+    nb = len(ip)
+    
+    if nb < 3:
+        # The minimal IPv6 address is :: so at least 3 parts after split
+        raise ValueError
+
+    if nbfull >= 1 and '.' in ip[-1]:
+        # Convert IPv4 mapped addresses to full hexadecimal notation
+        ipv4 = ip[-1].split('.')
+        hex1 = (int(ipv4[0]) << 8) + int(ipv4[1])
+        hex2 = (int(ipv4[2]) << 8) + int(ipv4[3])
+        ip[-1:] = [hex(hex1)[2:], hex(hex2)[2:]]
+        nbfull = nbfull + 1
+        nb = nb + 1
+
+    if nbfull == 8 or nbfull == nb:
+        # No need to bother
+        return addr
+    elif nbfull > 8:
+        # Has to be invalid anyway
+        raise ValueError
+
+    # Begin normalization
+    start, end, index = (None, None, 0)
+    for elem in ip:
+        if elem == '':
+            if start is None:
+                start = index
+                end = index
+            else:
+                end = index
+        index += 1
+    pad = 8 - nbfull
+    if end != start:
+        ip[start:end-start+1] = ['0'] * pad
+    else:
+        ip[start] = '0'
+        if pad > 1:
+            ip[start:1] = ['0'] * (pad - 1)
+    return ':'.join([item for item in ip if len(item) > 0])
diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py
index 927362a..6b68114 100644
--- a/tests/regressiontests/forms/tests/extra.py
+++ b/tests/regressiontests/forms/tests/extra.py
@@ -451,6 +451,18 @@ 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')
 
+        f = IP6AddressField()
+        self.assertEqual(f.clean('::1'), u'::1')
+        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.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7')
+        self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7:8:9')
+
         f = IPAddressField(required=False)
         self.assertEqual(f.clean(''), u'')
         self.assertEqual(f.clean(None), u'')
@@ -460,6 +472,16 @@ 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')
 
+        f = IP6AddressField(required=False)
+        self.assertEqual(f.clean('::1'), u'::1')
+        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.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7')
+        self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7:8:9')
+
     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 3a2c81a..4670185 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 IP6AddressData(models.Model):
+    data = models.IP6AddressField(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 IP6AddressPKData(models.Model):
+    data = models.IP6AddressField(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 97b2a79..5380c0f 100644
--- a/tests/regressiontests/serializers_regress/tests.py
+++ b/tests/regressiontests/serializers_regress/tests.py
@@ -196,6 +196,7 @@ test_data = [
     #(XX, ImageData
     (data_obj, 90, IPAddressData, "127.0.0.1"),
     (data_obj, 91, IPAddressData, None),
+    (data_obj, 92, IP6AddressData, "::1"),
     (data_obj, 100, NullBooleanData, True),
     (data_obj, 101, NullBooleanData, False),
     (data_obj, 102, NullBooleanData, None),
@@ -298,6 +299,7 @@ The end."""),
     (pk_obj, 682, IntegerPKData, 0),
 #     (XX, ImagePKData
     (pk_obj, 690, IPAddressPKData, "127.0.0.1"),
+    (pk_obj, 691, IP6AddressPKData, "::1"),
     # (pk_obj, 700, NullBooleanPKData, True),
     # (pk_obj, 701, NullBooleanPKData, False),
     (pk_obj, 710, PhonePKData, "212-634-5789"),
-- 
1.7.5+GitX