summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc Tamlyn <marc.tamlyn@gmail.com>2014-07-15 10:35:29 +0100
committerMarc Tamlyn <marc.tamlyn@gmail.com>2014-09-16 10:08:09 +0100
commited7821231b7dbf34a6c8ca65be3b9bcbda4a0703 (patch)
tree98657032a61094eb7ac9c46d5db5bc12d25d1fa0
parent0d1561d19739e83cf20150585c9e26894b428bad (diff)
Fixed #19463 -- Added UUIDField
Uses native support in postgres, and char(32) on other backends.
-rw-r--r--django/db/backends/mysql/base.py8
-rw-r--r--django/db/backends/mysql/creation.py1
-rw-r--r--django/db/backends/oracle/base.py8
-rw-r--r--django/db/backends/oracle/creation.py1
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py2
-rw-r--r--django/db/backends/postgresql_psycopg2/creation.py1
-rw-r--r--django/db/backends/sqlite3/base.py10
-rw-r--r--django/db/backends/sqlite3/creation.py1
-rw-r--r--django/db/models/fields/__init__.py43
-rw-r--r--django/forms/fields.py25
-rw-r--r--docs/ref/databases.txt3
-rw-r--r--docs/ref/forms/fields.txt14
-rw-r--r--docs/ref/models/fields.txt25
-rw-r--r--docs/releases/1.8.txt10
-rw-r--r--tests/forms_tests/tests/test_fields.py24
-rw-r--r--tests/model_fields/models.py13
-rw-r--r--tests/model_fields/test_uuid.py89
17 files changed, 274 insertions, 4 deletions
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index 69084aaa62..aefe4262e6 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -8,6 +8,7 @@ from __future__ import unicode_literals
import datetime
import re
import sys
+import uuid
import warnings
try:
@@ -398,6 +399,8 @@ class DatabaseOperations(BaseDatabaseOperations):
converters = super(DatabaseOperations, self).get_db_converters(internal_type)
if internal_type in ['BooleanField', 'NullBooleanField']:
converters.append(self.convert_booleanfield_value)
+ if internal_type == 'UUIDField':
+ converters.append(self.convert_uuidfield_value)
return converters
def convert_booleanfield_value(self, value, field):
@@ -405,6 +408,11 @@ class DatabaseOperations(BaseDatabaseOperations):
value = bool(value)
return value
+ def convert_uuidfield_value(self, value, field):
+ if value is not None:
+ value = uuid.UUID(value)
+ return value
+
class DatabaseWrapper(BaseDatabaseWrapper):
vendor = 'mysql'
diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py
index 565b1fd9a1..fd3a595eb4 100644
--- a/django/db/backends/mysql/creation.py
+++ b/django/db/backends/mysql/creation.py
@@ -30,6 +30,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'SmallIntegerField': 'smallint',
'TextField': 'longtext',
'TimeField': 'time',
+ 'UUIDField': 'char(32)',
}
def sql_table_creation_suffix(self):
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 17457dfcea..e48c82f0d8 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -10,6 +10,7 @@ import decimal
import re
import platform
import sys
+import uuid
import warnings
@@ -264,6 +265,8 @@ WHEN (new.%(col_name)s IS NULL)
converters.append(self.convert_datefield_value)
elif internal_type == 'TimeField':
converters.append(self.convert_timefield_value)
+ elif internal_type == 'UUIDField':
+ converters.append(self.convert_uuidfield_value)
converters.append(self.convert_empty_values)
return converters
@@ -310,6 +313,11 @@ WHEN (new.%(col_name)s IS NULL)
value = value.time()
return value
+ def convert_uuidfield_value(self, value, field):
+ if value is not None:
+ value = uuid.UUID(value)
+ return value
+
def deferrable_sql(self):
return " DEFERRABLE INITIALLY DEFERRED"
diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py
index 4b9146da7b..ef0156bc18 100644
--- a/django/db/backends/oracle/creation.py
+++ b/django/db/backends/oracle/creation.py
@@ -44,6 +44,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'TextField': 'NCLOB',
'TimeField': 'TIMESTAMP',
'URLField': 'VARCHAR2(%(max_length)s)',
+ 'UUIDField': 'VARCHAR2(32)',
}
data_type_check_constraints = {
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index 4cd327a22b..975eee4df2 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -22,6 +22,7 @@ from django.utils.timezone import utc
try:
import psycopg2 as Database
import psycopg2.extensions
+ import psycopg2.extras
except ImportError as e:
from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
@@ -33,6 +34,7 @@ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)
psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString)
psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
+psycopg2.extras.register_uuid()
def utc_tzinfo_factory(offset):
diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py
index 6a190bf092..9573107271 100644
--- a/django/db/backends/postgresql_psycopg2/creation.py
+++ b/django/db/backends/postgresql_psycopg2/creation.py
@@ -31,6 +31,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'SmallIntegerField': 'smallint',
'TextField': 'text',
'TimeField': 'time',
+ 'UUIDField': 'uuid',
}
data_type_check_constraints = {
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 1da2542382..0e1070e6e5 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -8,8 +8,9 @@ from __future__ import unicode_literals
import datetime
import decimal
-import warnings
import re
+import uuid
+import warnings
from django.conf import settings
from django.db import utils
@@ -273,6 +274,8 @@ class DatabaseOperations(BaseDatabaseOperations):
converters.append(self.convert_timefield_value)
elif internal_type == 'DecimalField':
converters.append(self.convert_decimalfield_value)
+ elif internal_type == 'UUIDField':
+ converters.append(self.convert_uuidfield_value)
return converters
def convert_decimalfield_value(self, value, field):
@@ -295,6 +298,11 @@ class DatabaseOperations(BaseDatabaseOperations):
value = parse_time(value)
return value
+ def convert_uuidfield_value(self, value, field):
+ if value is not None:
+ value = uuid.UUID(value)
+ return value
+
def bulk_insert_sql(self, fields, num_values):
res = []
res.append("SELECT %s" % ", ".join(
diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py
index 43b3924f4f..ea91c4a2b7 100644
--- a/django/db/backends/sqlite3/creation.py
+++ b/django/db/backends/sqlite3/creation.py
@@ -33,6 +33,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'SmallIntegerField': 'smallint',
'TextField': 'text',
'TimeField': 'time',
+ 'UUIDField': 'char(32)',
}
data_types_suffix = {
'AutoField': 'AUTOINCREMENT',
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index c41d4eaad2..de7631c246 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -6,6 +6,7 @@ import copy
import datetime
import decimal
import math
+import uuid
import warnings
from base64 import b64decode, b64encode
from itertools import tee
@@ -40,6 +41,7 @@ __all__ = [str(x) for x in (
'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'NOT_PROVIDED',
'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField',
'SlugField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField',
+ 'UUIDField',
)]
@@ -2217,3 +2219,44 @@ class BinaryField(Field):
if isinstance(value, six.text_type):
return six.memoryview(b64decode(force_bytes(value)))
return value
+
+
+class UUIDField(Field):
+ default_error_messages = {
+ 'invalid': _("'%(value)s' is not a valid UUID."),
+ }
+ description = 'Universally unique identifier'
+ empty_strings_allowed = False
+
+ def __init__(self, **kwargs):
+ kwargs['max_length'] = 32
+ super(UUIDField, self).__init__(**kwargs)
+
+ def get_internal_type(self):
+ return "UUIDField"
+
+ def get_prep_value(self, value):
+ if isinstance(value, uuid.UUID):
+ return value.hex
+ if isinstance(value, six.string_types):
+ return value.replace('-', '')
+ return value
+
+ def to_python(self, value):
+ if value and not isinstance(value, uuid.UUID):
+ try:
+ return uuid.UUID(value)
+ except ValueError:
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
+ return value
+
+ def formfield(self, **kwargs):
+ defaults = {
+ 'form_class': forms.UUIDField,
+ }
+ defaults.update(kwargs)
+ return super(UUIDField, self).formfield(**defaults)
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 2b44c73996..6403509fbb 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -9,6 +9,7 @@ import datetime
import os
import re
import sys
+import uuid
import warnings
from decimal import Decimal, DecimalException
from io import BytesIO
@@ -41,7 +42,7 @@ __all__ = (
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
'SplitDateTimeField', 'IPAddressField', 'GenericIPAddressField', 'FilePathField',
- 'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField'
+ 'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField', 'UUIDField',
)
@@ -1224,3 +1225,25 @@ class SlugField(CharField):
def clean(self, value):
value = self.to_python(value).strip()
return super(SlugField, self).clean(value)
+
+
+class UUIDField(CharField):
+ default_error_messages = {
+ 'invalid': _('Enter a valid UUID.'),
+ }
+
+ def prepare_value(self, value):
+ if isinstance(value, uuid.UUID):
+ return value.hex
+ return value
+
+ def to_python(self, value):
+ value = super(UUIDField, self).to_python(value)
+ if value in self.empty_values:
+ return None
+ if not isinstance(value, uuid.UUID):
+ try:
+ value = uuid.UUID(value)
+ except ValueError:
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
+ return value
diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt
index ca909fcd04..7d1578bc59 100644
--- a/docs/ref/databases.txt
+++ b/docs/ref/databases.txt
@@ -92,7 +92,8 @@ below for information on how to set up your database correctly.
PostgreSQL notes
================
-Django supports PostgreSQL 9.0 and higher.
+Django supports PostgreSQL 9.0 and higher. It requires the use of Psycopg2
+2.0.9 or higher.
PostgreSQL connection settings
-------------------------------
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index bd7cb93389..d7fcef2e75 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -888,6 +888,20 @@ For each field, we describe the default widget used if you don't specify
These are the same as ``CharField.max_length`` and ``CharField.min_length``.
+``UUIDField``
+-------------
+
+.. versionadded:: 1.8
+
+.. class:: UUIDField(**kwargs)
+
+ * Default widget: :class:`TextInput`
+ * Empty value: ``''`` (an empty string)
+ * Normalizes to: A :class:`~python:uuid.UUID` object.
+ * Error message keys: ``required``, ``invalid``
+
+ This field will accept any string format accepted as the ``hex`` argument
+ to the :class:`~python:uuid.UUID` constructor.
Slightly complex built-in ``Field`` classes
-------------------------------------------
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index e7717e9d90..fe35f979ca 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -1012,6 +1012,31 @@ Like all :class:`CharField` subclasses, :class:`URLField` takes the optional
:attr:`~CharField.max_length` argument. If you don't specify
:attr:`~CharField.max_length`, a default of 200 is used.
+UUIDField
+---------
+
+.. versionadded:: 1.8
+
+.. class:: UUIDField([**options])
+
+A field for storing universally unique identifiers. Uses Python's
+:class:`~python:uuid.UUID` class. When used on PostgreSQL, this stores in a
+``uuid`` datatype, otherwise in a ``char(32)``.
+
+Universally unique identifiers are a good alternative to :class:`AutoField` for
+:attr:`~Field.primary_key`. The database will not generate the UUID for you, so
+it is recommended to use :attr:`~Field.default`::
+
+ import uuid
+ from django.db import models
+
+ class MyUUIDModel(models.Model):
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+ # other fields
+
+Note that a callable (with the parentheses omitted) is passed to ``default``,
+not an instance of ``UUID``.
+
Relationship fields
===================
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index 51febddd9a..a8e9ce8015 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -35,6 +35,14 @@ site.
.. _django-secure: https://pypi.python.org/pypi/django-secure
+New data types
+~~~~~~~~~~~~~~
+
+* Django now has a :class:`~django.db.models.UUIDField` for storing
+ universally unique identifiers. There is a corresponding :class:`form field
+ <django.forms.UUIDField>`. It is stored as the native ``uuid`` data type on
+ PostgreSQL and as a fixed length character field on other backends.
+
Minor features
~~~~~~~~~~~~~~
@@ -474,6 +482,8 @@ officially supports.
This also includes dropping support for PostGIS 1.3 and 1.4 as these versions
are not supported on versions of PostgreSQL later than 8.4.
+Django also now requires the use of Psycopg2 version 2.0.9 or higher.
+
Support for MySQL versions older than 5.5
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py
index b64e55b066..22af2cf23d 100644
--- a/tests/forms_tests/tests/test_fields.py
+++ b/tests/forms_tests/tests/test_fields.py
@@ -30,6 +30,7 @@ import datetime
import pickle
import re
import os
+import uuid
from decimal import Decimal
from unittest import skipIf
import warnings
@@ -46,7 +47,7 @@ from django.forms import (
Form, forms, HiddenInput, ImageField, IntegerField, MultipleChoiceField,
NullBooleanField, NumberInput, PasswordInput, RadioSelect, RegexField,
SplitDateTimeField, TextInput, Textarea, TimeField, TypedChoiceField,
- TypedMultipleChoiceField, URLField, ValidationError, Widget,
+ TypedMultipleChoiceField, URLField, UUIDField, ValidationError, Widget,
)
from django.test import SimpleTestCase
from django.utils import formats
@@ -1342,3 +1343,24 @@ class FieldsTests(SimpleTestCase):
self.assertTrue(f.has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00']))
self.assertFalse(f.has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40']))
self.assertTrue(f.has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41']))
+
+ def test_uuidfield_1(self):
+ field = UUIDField()
+ value = field.clean('550e8400e29b41d4a716446655440000')
+ self.assertEqual(value, uuid.UUID('550e8400e29b41d4a716446655440000'))
+
+ def test_uuidfield_2(self):
+ field = UUIDField(required=False)
+ value = field.clean('')
+ self.assertEqual(value, None)
+
+ def test_uuidfield_3(self):
+ field = UUIDField()
+ with self.assertRaises(ValidationError) as cm:
+ field.clean('550e8400')
+ self.assertEqual(cm.exception.messages[0], 'Enter a valid UUID.')
+
+ def test_uuidfield_4(self):
+ field = UUIDField()
+ value = field.prepare_value(uuid.UUID('550e8400e29b41d4a716446655440000'))
+ self.assertEqual(value, '550e8400e29b41d4a716446655440000')
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 430722cf4b..ed1cfa8e8d 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -1,5 +1,6 @@
import os
import tempfile
+import uuid
import warnings
try:
@@ -294,3 +295,15 @@ if Image:
width_field='headshot_width')
###############################################################################
+
+
+class UUIDModel(models.Model):
+ field = models.UUIDField()
+
+
+class NullableUUIDModel(models.Model):
+ field = models.UUIDField(blank=True, null=True)
+
+
+class PrimaryKeyUUIDModel(models.Model):
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4)
diff --git a/tests/model_fields/test_uuid.py b/tests/model_fields/test_uuid.py
new file mode 100644
index 0000000000..b5bc37a48d
--- /dev/null
+++ b/tests/model_fields/test_uuid.py
@@ -0,0 +1,89 @@
+import json
+import uuid
+
+from django.core import exceptions, serializers
+from django.db import models
+from django.test import TestCase
+
+from .models import UUIDModel, NullableUUIDModel, PrimaryKeyUUIDModel
+
+
+class TestSaveLoad(TestCase):
+ def test_uuid_instance(self):
+ instance = UUIDModel.objects.create(field=uuid.uuid4())
+ loaded = UUIDModel.objects.get()
+ self.assertEqual(loaded.field, instance.field)
+
+ def test_str_instance_no_hyphens(self):
+ UUIDModel.objects.create(field='550e8400e29b41d4a716446655440000')
+ loaded = UUIDModel.objects.get()
+ self.assertEqual(loaded.field, uuid.UUID('550e8400e29b41d4a716446655440000'))
+
+ def test_str_instance_hyphens(self):
+ UUIDModel.objects.create(field='550e8400-e29b-41d4-a716-446655440000')
+ loaded = UUIDModel.objects.get()
+ self.assertEqual(loaded.field, uuid.UUID('550e8400e29b41d4a716446655440000'))
+
+ def test_str_instance_bad_hyphens(self):
+ UUIDModel.objects.create(field='550e84-00-e29b-41d4-a716-4-466-55440000')
+ loaded = UUIDModel.objects.get()
+ self.assertEqual(loaded.field, uuid.UUID('550e8400e29b41d4a716446655440000'))
+
+ def test_null_handling(self):
+ NullableUUIDModel.objects.create(field=None)
+ loaded = NullableUUIDModel.objects.get()
+ self.assertEqual(loaded.field, None)
+
+
+class TestQuerying(TestCase):
+ def setUp(self):
+ self.objs = [
+ NullableUUIDModel.objects.create(field=uuid.uuid4()),
+ NullableUUIDModel.objects.create(field='550e8400e29b41d4a716446655440000'),
+ NullableUUIDModel.objects.create(field=None),
+ ]
+
+ def test_exact(self):
+ self.assertSequenceEqual(
+ NullableUUIDModel.objects.filter(field__exact='550e8400e29b41d4a716446655440000'),
+ [self.objs[1]]
+ )
+
+ def test_isnull(self):
+ self.assertSequenceEqual(
+ NullableUUIDModel.objects.filter(field__isnull=True),
+ [self.objs[2]]
+ )
+
+
+class TestSerialization(TestCase):
+ test_data = '[{"fields": {"field": "550e8400-e29b-41d4-a716-446655440000"}, "model": "model_fields.uuidmodel", "pk": null}]'
+
+ def test_dumping(self):
+ instance = UUIDModel(field=uuid.UUID('550e8400e29b41d4a716446655440000'))
+ data = serializers.serialize('json', [instance])
+ self.assertEqual(json.loads(data), json.loads(self.test_data))
+
+ def test_loading(self):
+ instance = list(serializers.deserialize('json', self.test_data))[0].object
+ self.assertEqual(instance.field, uuid.UUID('550e8400-e29b-41d4-a716-446655440000'))
+
+
+class TestValidation(TestCase):
+ def test_invalid_uuid(self):
+ field = models.UUIDField()
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('550e8400', None)
+ self.assertEqual(cm.exception.code, 'invalid')
+ self.assertEqual(cm.exception.message % cm.exception.params, "'550e8400' is not a valid UUID.")
+
+ def test_uuid_instance_ok(self):
+ field = models.UUIDField()
+ field.clean(uuid.uuid4(), None) # no error
+
+
+class TestAsPrimaryKey(TestCase):
+ def test_creation(self):
+ PrimaryKeyUUIDModel.objects.create()
+ loaded = PrimaryKeyUUIDModel.objects.get()
+ self.assertIsInstance(loaded.pk, uuid.UUID)