Permalink
Browse files

Fixed #19463 -- Added UUIDField

Uses native support in postgres, and char(32) on other backends.
  • Loading branch information...
mjtamlyn committed Jul 15, 2014
1 parent 0d1561d commit ed7821231b7dbf34a6c8ca65be3b9bcbda4a0703
@@ -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':

This comment has been minimized.

Show comment
Hide comment
@dotsbb

dotsbb Sep 16, 2014

if -> elif

@dotsbb
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)
View
@@ -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
View
@@ -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
@@ -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
@@ -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.