Fixed #19463 -- Added UUIDField

Uses native support in postgres, and char(32) on other backends.
This commit is contained in:
Marc Tamlyn 2014-07-15 10:35:29 +01:00
parent 0d1561d197
commit ed7821231b
17 changed files with 274 additions and 4 deletions

View File

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

View File

@ -30,6 +30,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'SmallIntegerField': 'smallint',
'TextField': 'longtext',
'TimeField': 'time',
'UUIDField': 'char(32)',
}
def sql_table_creation_suffix(self):

View File

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

View File

@ -44,6 +44,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'TextField': 'NCLOB',
'TimeField': 'TIMESTAMP',
'URLField': 'VARCHAR2(%(max_length)s)',
'UUIDField': 'VARCHAR2(32)',
}
data_type_check_constraints = {

View File

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

View File

@ -31,6 +31,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'SmallIntegerField': 'smallint',
'TextField': 'text',
'TimeField': 'time',
'UUIDField': 'uuid',
}
data_type_check_constraints = {

View File

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

View File

@ -33,6 +33,7 @@ class DatabaseCreation(BaseDatabaseCreation):
'SmallIntegerField': 'smallint',
'TextField': 'text',
'TimeField': 'time',
'UUIDField': 'char(32)',
}
data_types_suffix = {
'AutoField': 'AUTOINCREMENT',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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