Fixed #19463 -- Added UUIDField
Uses native support in postgres, and char(32) on other backends.
This commit is contained in:
parent
0d1561d197
commit
ed7821231b
|
@ -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'
|
||||
|
|
|
@ -30,6 +30,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
'SmallIntegerField': 'smallint',
|
||||
'TextField': 'longtext',
|
||||
'TimeField': 'time',
|
||||
'UUIDField': 'char(32)',
|
||||
}
|
||||
|
||||
def sql_table_creation_suffix(self):
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
'TextField': 'NCLOB',
|
||||
'TimeField': 'TIMESTAMP',
|
||||
'URLField': 'VARCHAR2(%(max_length)s)',
|
||||
'UUIDField': 'VARCHAR2(32)',
|
||||
}
|
||||
|
||||
data_type_check_constraints = {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -31,6 +31,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
'SmallIntegerField': 'smallint',
|
||||
'TextField': 'text',
|
||||
'TimeField': 'time',
|
||||
'UUIDField': 'uuid',
|
||||
}
|
||||
|
||||
data_type_check_constraints = {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -33,6 +33,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
|||
'SmallIntegerField': 'smallint',
|
||||
'TextField': 'text',
|
||||
'TimeField': 'time',
|
||||
'UUIDField': 'char(32)',
|
||||
}
|
||||
data_types_suffix = {
|
||||
'AutoField': 'AUTOINCREMENT',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
-------------------------------
|
||||
|
|
|
@ -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
|
||||
-------------------------------------------
|
||||
|
|
|
@ -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
|
||||
===================
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue