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
|
b
|
import urlparse
|
5 | 5 | from django.core.exceptions import ValidationError |
6 | 6 | from django.utils.translation import ugettext_lazy as _ |
7 | 7 | from django.utils.encoding import smart_unicode |
| 8 | from django.utils.ip import ipv6_normalize |
8 | 9 | |
9 | 10 | # These values, if given to validate(), will trigger the self.required check. |
10 | 11 | EMPTY_VALUES = (None, '', [], (), {}) |
… |
… |
class RegexValidator(object):
|
20 | 21 | regex = '' |
21 | 22 | message = _(u'Enter a valid value.') |
22 | 23 | code = 'invalid' |
| 24 | normalizer = None |
23 | 25 | |
24 | | def __init__(self, regex=None, message=None, code=None): |
| 26 | def __init__(self, regex=None, message=None, code=None, normalizer=None): |
25 | 27 | if regex is not None: |
26 | 28 | self.regex = regex |
27 | 29 | if message is not None: |
28 | 30 | self.message = message |
29 | 31 | if code is not None: |
30 | 32 | self.code = code |
| 33 | if normalizer is not None: |
| 34 | self.normalizer = normalizer |
31 | 35 | |
32 | 36 | if isinstance(self.regex, basestring): |
33 | 37 | self.regex = re.compile(regex) |
… |
… |
class RegexValidator(object):
|
36 | 40 | """ |
37 | 41 | Validates that the input matches the regular expression. |
38 | 42 | """ |
| 43 | if self.normalizer: |
| 44 | try: |
| 45 | value = self.normalizer(value) |
| 46 | except ValueError: |
| 47 | raise ValidationError(self.message, code=self.code) |
| 48 | |
39 | 49 | if not self.regex.search(smart_unicode(value)): |
40 | 50 | raise ValidationError(self.message, code=self.code) |
41 | 51 | |
… |
… |
validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of l
|
143 | 153 | 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}$') |
144 | 154 | validate_ipv4_address = RegexValidator(ipv4_re, _(u'Enter a valid IPv4 address.'), 'invalid') |
145 | 155 | |
| 156 | ipv6_re = re.compile(r'^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$') |
| 157 | validate_ipv6_address = RegexValidator(ipv6_re, _(u'Enter a valid IPv6 address.'), 'invalid', ipv6_normalize) |
| 158 | |
| 159 | def validate_ip_address(value): |
| 160 | try: |
| 161 | validate_ipv4_address(value) |
| 162 | except ValidationError: |
| 163 | try: |
| 164 | validate_ipv6_address(value) |
| 165 | except ValidationError: |
| 166 | raise ValidationError(_(u'Enter a valid IP address.'), code='invalid') |
| 167 | |
146 | 168 | comma_separated_int_list_re = re.compile('^[\d,]+$') |
147 | 169 | validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid') |
148 | 170 | |
diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py
index 8b026a9..5fa71be 100644
a
|
b
|
class DatabaseCreation(BaseDatabaseCreation):
|
19 | 19 | 'IntegerField': 'integer', |
20 | 20 | 'BigIntegerField': 'bigint', |
21 | 21 | 'IPAddressField': 'char(15)', |
| 22 | 'IP6AddressField': 'char(39)', |
22 | 23 | 'NullBooleanField': 'bool', |
23 | 24 | 'OneToOneField': 'integer', |
24 | 25 | 'PositiveIntegerField': 'integer UNSIGNED', |
diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py
index 29293db..2ff4c94 100644
a
|
b
|
class DatabaseCreation(BaseDatabaseCreation):
|
27 | 27 | 'IntegerField': 'NUMBER(11)', |
28 | 28 | 'BigIntegerField': 'NUMBER(19)', |
29 | 29 | 'IPAddressField': 'VARCHAR2(15)', |
| 30 | 'IP6AddressField': 'VARCHAR2(39)', |
30 | 31 | 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))', |
31 | 32 | 'OneToOneField': 'NUMBER(11)', |
32 | 33 | '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
|
b
|
class DatabaseCreation(BaseDatabaseCreation):
|
21 | 21 | 'IntegerField': 'integer', |
22 | 22 | 'BigIntegerField': 'bigint', |
23 | 23 | 'IPAddressField': 'inet', |
| 24 | 'IP6AddressField': 'inet', |
24 | 25 | 'NullBooleanField': 'boolean', |
25 | 26 | 'OneToOneField': 'integer', |
26 | 27 | '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
|
b
|
class DatabaseCreation(BaseDatabaseCreation):
|
20 | 20 | 'IntegerField': 'integer', |
21 | 21 | 'BigIntegerField': 'bigint', |
22 | 22 | 'IPAddressField': 'char(15)', |
| 23 | 'IP6AddressField': 'char(39)', |
23 | 24 | 'NullBooleanField': 'bool', |
24 | 25 | 'OneToOneField': 'integer', |
25 | 26 | 'PositiveIntegerField': 'integer unsigned', |
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 9037265..c7f020d 100644
a
|
b
|
class BigIntegerField(IntegerField):
|
919 | 919 | |
920 | 920 | class IPAddressField(Field): |
921 | 921 | empty_strings_allowed = False |
922 | | description = _("IP address") |
| 922 | description = _("IPv4 address") |
923 | 923 | def __init__(self, *args, **kwargs): |
924 | 924 | kwargs['max_length'] = 15 |
925 | 925 | Field.__init__(self, *args, **kwargs) |
… |
… |
class IPAddressField(Field):
|
932 | 932 | defaults.update(kwargs) |
933 | 933 | return super(IPAddressField, self).formfield(**defaults) |
934 | 934 | |
| 935 | class IP6AddressField(Field): |
| 936 | empty_strings_allowed = False |
| 937 | description = _("IPv6 address") |
| 938 | def __init__(self, *args, **kwargs): |
| 939 | kwargs['max_length'] = 39 |
| 940 | Field.__init__(self, *args, **kwargs) |
| 941 | |
| 942 | def get_internal_type(self): |
| 943 | return "IP6AddressField" |
| 944 | |
| 945 | def formfield(self, **kwargs): |
| 946 | defaults = {'form_class': forms.IP6AddressField} |
| 947 | defaults.update(kwargs) |
| 948 | return super(IP6AddressField, self).formfield(**defaults) |
| 949 | |
| 950 | |
935 | 951 | class NullBooleanField(Field): |
936 | 952 | empty_strings_allowed = False |
937 | 953 | default_error_messages = { |
diff --git a/django/forms/fields.py b/django/forms/fields.py
index a5ea81d..11fd416 100644
a
|
b
|
__all__ = (
|
38 | 38 | 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', |
39 | 39 | 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', |
40 | 40 | 'SplitDateTimeField', 'IPAddressField', 'FilePathField', 'SlugField', |
41 | | 'TypedChoiceField', 'TypedMultipleChoiceField' |
| 41 | 'TypedChoiceField', 'TypedMultipleChoiceField', 'IP6AddressField', |
42 | 42 | ) |
43 | 43 | |
44 | 44 | |
… |
… |
class IPAddressField(CharField):
|
946 | 946 | } |
947 | 947 | default_validators = [validators.validate_ipv4_address] |
948 | 948 | |
| 949 | class IP6AddressField(CharField): |
| 950 | default_error_messages = { |
| 951 | 'invalid': _(u'Enter a valid IPv6 address.'), |
| 952 | } |
| 953 | default_validators = [validators.validate_ipv6_address] |
| 954 | |
949 | 955 | |
950 | 956 | class SlugField(CharField): |
951 | 957 | default_error_messages = { |
diff --git a/django/utils/ip.py b/django/utils/ip.py
new file mode 100644
index 0000000..5929689
-
|
+
|
|
| 1 | def ipv6_normalize(addr): |
| 2 | """ |
| 3 | Normalize an IPv6 address to allow easy regexp validation. |
| 4 | Mostly checks the length, and gets rid of tricky things |
| 5 | like IPv4 mapped addresses and :: shortcuts |
| 6 | |
| 7 | Outputs a string |
| 8 | """ |
| 9 | # Some basic error checking |
| 10 | if addr.count('::') > 2 or ':::' in addr: |
| 11 | raise ValueError |
| 12 | |
| 13 | ip = addr.split(':') |
| 14 | nbfull = len([elem for elem in ip if elem != '']) |
| 15 | nb = len(ip) |
| 16 | |
| 17 | if nb < 3: |
| 18 | # The minimal IPv6 address is :: so at least 3 parts after split |
| 19 | raise ValueError |
| 20 | |
| 21 | if nbfull >= 1 and '.' in ip[-1]: |
| 22 | # Convert IPv4 mapped addresses to full hexadecimal notation |
| 23 | ipv4 = ip[-1].split('.') |
| 24 | hex1 = (int(ipv4[0]) << 8) + int(ipv4[1]) |
| 25 | hex2 = (int(ipv4[2]) << 8) + int(ipv4[3]) |
| 26 | ip[-1:] = [hex(hex1)[2:], hex(hex2)[2:]] |
| 27 | nbfull = nbfull + 1 |
| 28 | nb = nb + 1 |
| 29 | |
| 30 | if nbfull == 8 or nbfull == nb: |
| 31 | # No need to bother |
| 32 | return addr |
| 33 | elif nbfull > 8: |
| 34 | # Has to be invalid anyway |
| 35 | raise ValueError |
| 36 | |
| 37 | # Begin normalization |
| 38 | start, end, index = (None, None, 0) |
| 39 | for elem in ip: |
| 40 | if elem == '': |
| 41 | if start is None: |
| 42 | start = index |
| 43 | end = index |
| 44 | else: |
| 45 | end = index |
| 46 | index += 1 |
| 47 | pad = 8 - nbfull |
| 48 | if end != start: |
| 49 | ip[start:end-start+1] = ['0'] * pad |
| 50 | else: |
| 51 | ip[start] = '0' |
| 52 | if pad > 1: |
| 53 | ip[start:1] = ['0'] * (pad - 1) |
| 54 | 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
|
b
|
class FormsExtraTestCase(unittest.TestCase, AssertFormErrorsMixin):
|
451 | 451 | self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5') |
452 | 452 | self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '256.125.1.5') |
453 | 453 | |
| 454 | f = IP6AddressField() |
| 455 | self.assertEqual(f.clean('::1'), u'::1') |
| 456 | self.assertFormErrors([u'This field is required.'], f.clean, '') |
| 457 | self.assertFormErrors([u'This field is required.'], f.clean, None) |
| 458 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '127.0.0.1') |
| 459 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, 'foo') |
| 460 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '127.0.0.') |
| 461 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1.2.3.4.5') |
| 462 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '256.125.1.5') |
| 463 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7') |
| 464 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7:8:9') |
| 465 | |
454 | 466 | f = IPAddressField(required=False) |
455 | 467 | self.assertEqual(f.clean(''), u'') |
456 | 468 | self.assertEqual(f.clean(None), u'') |
… |
… |
class FormsExtraTestCase(unittest.TestCase, AssertFormErrorsMixin):
|
460 | 472 | self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5') |
461 | 473 | self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '256.125.1.5') |
462 | 474 | |
| 475 | f = IP6AddressField(required=False) |
| 476 | self.assertEqual(f.clean('::1'), u'::1') |
| 477 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '127.0.0.1') |
| 478 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, 'foo') |
| 479 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '127.0.0.') |
| 480 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1.2.3.4.5') |
| 481 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '256.125.1.5') |
| 482 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7') |
| 483 | self.assertFormErrors([u'Enter a valid IPv6 address.'], f.clean, '1:2:3:4:5:6:7:8:9') |
| 484 | |
463 | 485 | def test_smart_unicode(self): |
464 | 486 | class Test: |
465 | 487 | def __str__(self): |
diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py
index 3a2c81a..4670185 100644
a
|
b
|
class BigIntegerData(models.Model):
|
52 | 52 | class IPAddressData(models.Model): |
53 | 53 | data = models.IPAddressField(null=True) |
54 | 54 | |
| 55 | class IP6AddressData(models.Model): |
| 56 | data = models.IP6AddressField(null=True) |
| 57 | |
55 | 58 | class NullBooleanData(models.Model): |
56 | 59 | data = models.NullBooleanField(null=True) |
57 | 60 | |
… |
… |
class IntegerPKData(models.Model):
|
187 | 190 | class IPAddressPKData(models.Model): |
188 | 191 | data = models.IPAddressField(primary_key=True) |
189 | 192 | |
| 193 | class IP6AddressPKData(models.Model): |
| 194 | data = models.IP6AddressField(primary_key=True) |
| 195 | |
190 | 196 | # This is just a Boolean field with null=True, and we can't test a PK value of NULL. |
191 | 197 | # class NullBooleanPKData(models.Model): |
192 | 198 | # 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
|
b
|
test_data = [
|
196 | 196 | #(XX, ImageData |
197 | 197 | (data_obj, 90, IPAddressData, "127.0.0.1"), |
198 | 198 | (data_obj, 91, IPAddressData, None), |
| 199 | (data_obj, 92, IP6AddressData, "::1"), |
199 | 200 | (data_obj, 100, NullBooleanData, True), |
200 | 201 | (data_obj, 101, NullBooleanData, False), |
201 | 202 | (data_obj, 102, NullBooleanData, None), |
… |
… |
The end."""),
|
298 | 299 | (pk_obj, 682, IntegerPKData, 0), |
299 | 300 | # (XX, ImagePKData |
300 | 301 | (pk_obj, 690, IPAddressPKData, "127.0.0.1"), |
| 302 | (pk_obj, 691, IP6AddressPKData, "::1"), |
301 | 303 | # (pk_obj, 700, NullBooleanPKData, True), |
302 | 304 | # (pk_obj, 701, NullBooleanPKData, False), |
303 | 305 | (pk_obj, 710, PhonePKData, "212-634-5789"), |