Permalink
Browse files

Fixed #19463 -- Added UUIDField

Uses native support in postgres, and char(32) on other backends.
  • Loading branch information...
1 parent 0d1561d commit ed7821231b7dbf34a6c8ca65be3b9bcbda4a0703 @mjtamlyn mjtamlyn committed Jul 15, 2014
@@ -8,6 +8,7 @@
import datetime
import re
import sys
+import uuid
import warnings
try:
@@ -398,13 +399,20 @@ def get_db_converters(self, internal_type):
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):
if value in (0, 1):
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 re
import platform
import sys
+import uuid
import warnings
@@ -264,6 +265,8 @@ def get_db_converters(self, internal_type):
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 @@ def convert_timefield_value(self, value, field):
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 @@
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.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 @@
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 @@ def get_db_converters(self, internal_type):
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 @@ def convert_timefield_value(self, value, field):
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 datetime
import decimal
import math
+import uuid
import warnings
from base64 import b64decode, b64encode
from itertools import tee
@@ -40,6 +41,7 @@
'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'NOT_PROVIDED',
'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField',
'SlugField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField',
+ 'UUIDField',
)]
@@ -2217,3 +2219,44 @@ def to_python(self, value):
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 os
import re
import sys
+import uuid
import warnings
from decimal import Decimal, DecimalException
from io import BytesIO
@@ -41,7 +42,7 @@
'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
===================
View
@@ -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 pickle
import re
import os
+import uuid
from decimal import Decimal
from unittest import skipIf
import warnings
@@ -46,7 +47,7 @@
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 @@ def test_splitdatetimefield_changed(self):
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 @@ class PersonTwoImages(models.Model):
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)
Oops, something went wrong.

0 comments on commit ed78212

Please sign in to comment.