mirror of https://github.com/django/django.git
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 datetime
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -398,6 +399,8 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
converters = super(DatabaseOperations, self).get_db_converters(internal_type)
|
converters = super(DatabaseOperations, self).get_db_converters(internal_type)
|
||||||
if internal_type in ['BooleanField', 'NullBooleanField']:
|
if internal_type in ['BooleanField', 'NullBooleanField']:
|
||||||
converters.append(self.convert_booleanfield_value)
|
converters.append(self.convert_booleanfield_value)
|
||||||
|
if internal_type == 'UUIDField':
|
||||||
|
converters.append(self.convert_uuidfield_value)
|
||||||
return converters
|
return converters
|
||||||
|
|
||||||
def convert_booleanfield_value(self, value, field):
|
def convert_booleanfield_value(self, value, field):
|
||||||
|
@ -405,6 +408,11 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
value = bool(value)
|
value = bool(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def convert_uuidfield_value(self, value, field):
|
||||||
|
if value is not None:
|
||||||
|
value = uuid.UUID(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
vendor = 'mysql'
|
vendor = 'mysql'
|
||||||
|
|
|
@ -30,6 +30,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'SmallIntegerField': 'smallint',
|
'SmallIntegerField': 'smallint',
|
||||||
'TextField': 'longtext',
|
'TextField': 'longtext',
|
||||||
'TimeField': 'time',
|
'TimeField': 'time',
|
||||||
|
'UUIDField': 'char(32)',
|
||||||
}
|
}
|
||||||
|
|
||||||
def sql_table_creation_suffix(self):
|
def sql_table_creation_suffix(self):
|
||||||
|
|
|
@ -10,6 +10,7 @@ import decimal
|
||||||
import re
|
import re
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
@ -264,6 +265,8 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
converters.append(self.convert_datefield_value)
|
converters.append(self.convert_datefield_value)
|
||||||
elif internal_type == 'TimeField':
|
elif internal_type == 'TimeField':
|
||||||
converters.append(self.convert_timefield_value)
|
converters.append(self.convert_timefield_value)
|
||||||
|
elif internal_type == 'UUIDField':
|
||||||
|
converters.append(self.convert_uuidfield_value)
|
||||||
converters.append(self.convert_empty_values)
|
converters.append(self.convert_empty_values)
|
||||||
return converters
|
return converters
|
||||||
|
|
||||||
|
@ -310,6 +313,11 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
value = value.time()
|
value = value.time()
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def convert_uuidfield_value(self, value, field):
|
||||||
|
if value is not None:
|
||||||
|
value = uuid.UUID(value)
|
||||||
|
return value
|
||||||
|
|
||||||
def deferrable_sql(self):
|
def deferrable_sql(self):
|
||||||
return " DEFERRABLE INITIALLY DEFERRED"
|
return " DEFERRABLE INITIALLY DEFERRED"
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'TextField': 'NCLOB',
|
'TextField': 'NCLOB',
|
||||||
'TimeField': 'TIMESTAMP',
|
'TimeField': 'TIMESTAMP',
|
||||||
'URLField': 'VARCHAR2(%(max_length)s)',
|
'URLField': 'VARCHAR2(%(max_length)s)',
|
||||||
|
'UUIDField': 'VARCHAR2(32)',
|
||||||
}
|
}
|
||||||
|
|
||||||
data_type_check_constraints = {
|
data_type_check_constraints = {
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django.utils.timezone import utc
|
||||||
try:
|
try:
|
||||||
import psycopg2 as Database
|
import psycopg2 as Database
|
||||||
import psycopg2.extensions
|
import psycopg2.extensions
|
||||||
|
import psycopg2.extras
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
|
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_type(psycopg2.extensions.UNICODEARRAY)
|
||||||
psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString)
|
psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString)
|
||||||
psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
|
psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
|
||||||
|
psycopg2.extras.register_uuid()
|
||||||
|
|
||||||
|
|
||||||
def utc_tzinfo_factory(offset):
|
def utc_tzinfo_factory(offset):
|
||||||
|
|
|
@ -31,6 +31,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'SmallIntegerField': 'smallint',
|
'SmallIntegerField': 'smallint',
|
||||||
'TextField': 'text',
|
'TextField': 'text',
|
||||||
'TimeField': 'time',
|
'TimeField': 'time',
|
||||||
|
'UUIDField': 'uuid',
|
||||||
}
|
}
|
||||||
|
|
||||||
data_type_check_constraints = {
|
data_type_check_constraints = {
|
||||||
|
|
|
@ -8,8 +8,9 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import warnings
|
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import utils
|
from django.db import utils
|
||||||
|
@ -273,6 +274,8 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
converters.append(self.convert_timefield_value)
|
converters.append(self.convert_timefield_value)
|
||||||
elif internal_type == 'DecimalField':
|
elif internal_type == 'DecimalField':
|
||||||
converters.append(self.convert_decimalfield_value)
|
converters.append(self.convert_decimalfield_value)
|
||||||
|
elif internal_type == 'UUIDField':
|
||||||
|
converters.append(self.convert_uuidfield_value)
|
||||||
return converters
|
return converters
|
||||||
|
|
||||||
def convert_decimalfield_value(self, value, field):
|
def convert_decimalfield_value(self, value, field):
|
||||||
|
@ -295,6 +298,11 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
value = parse_time(value)
|
value = parse_time(value)
|
||||||
return 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):
|
def bulk_insert_sql(self, fields, num_values):
|
||||||
res = []
|
res = []
|
||||||
res.append("SELECT %s" % ", ".join(
|
res.append("SELECT %s" % ", ".join(
|
||||||
|
|
|
@ -33,6 +33,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
'SmallIntegerField': 'smallint',
|
'SmallIntegerField': 'smallint',
|
||||||
'TextField': 'text',
|
'TextField': 'text',
|
||||||
'TimeField': 'time',
|
'TimeField': 'time',
|
||||||
|
'UUIDField': 'char(32)',
|
||||||
}
|
}
|
||||||
data_types_suffix = {
|
data_types_suffix = {
|
||||||
'AutoField': 'AUTOINCREMENT',
|
'AutoField': 'AUTOINCREMENT',
|
||||||
|
|
|
@ -6,6 +6,7 @@ import copy
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import math
|
import math
|
||||||
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from itertools import tee
|
from itertools import tee
|
||||||
|
@ -40,6 +41,7 @@ __all__ = [str(x) for x in (
|
||||||
'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'NOT_PROVIDED',
|
'GenericIPAddressField', 'IPAddressField', 'IntegerField', 'NOT_PROVIDED',
|
||||||
'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField',
|
'NullBooleanField', 'PositiveIntegerField', 'PositiveSmallIntegerField',
|
||||||
'SlugField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField',
|
'SlugField', 'SmallIntegerField', 'TextField', 'TimeField', 'URLField',
|
||||||
|
'UUIDField',
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
@ -2217,3 +2219,44 @@ class BinaryField(Field):
|
||||||
if isinstance(value, six.text_type):
|
if isinstance(value, six.text_type):
|
||||||
return six.memoryview(b64decode(force_bytes(value)))
|
return six.memoryview(b64decode(force_bytes(value)))
|
||||||
return 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 os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from decimal import Decimal, DecimalException
|
from decimal import Decimal, DecimalException
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
@ -41,7 +42,7 @@ __all__ = (
|
||||||
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
|
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
|
||||||
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
|
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
|
||||||
'SplitDateTimeField', 'IPAddressField', 'GenericIPAddressField', 'FilePathField',
|
'SplitDateTimeField', 'IPAddressField', 'GenericIPAddressField', 'FilePathField',
|
||||||
'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField'
|
'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField', 'UUIDField',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1224,3 +1225,25 @@ class SlugField(CharField):
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
value = self.to_python(value).strip()
|
value = self.to_python(value).strip()
|
||||||
return super(SlugField, self).clean(value)
|
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
|
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
|
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``.
|
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
|
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` argument. If you don't specify
|
||||||
:attr:`~CharField.max_length`, a default of 200 is used.
|
: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
|
Relationship fields
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,14 @@ site.
|
||||||
|
|
||||||
.. _django-secure: https://pypi.python.org/pypi/django-secure
|
.. _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
|
Minor features
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -474,6 +482,8 @@ officially supports.
|
||||||
This also includes dropping support for PostGIS 1.3 and 1.4 as these versions
|
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.
|
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
|
Support for MySQL versions older than 5.5
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import datetime
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from unittest import skipIf
|
from unittest import skipIf
|
||||||
import warnings
|
import warnings
|
||||||
|
@ -46,7 +47,7 @@ from django.forms import (
|
||||||
Form, forms, HiddenInput, ImageField, IntegerField, MultipleChoiceField,
|
Form, forms, HiddenInput, ImageField, IntegerField, MultipleChoiceField,
|
||||||
NullBooleanField, NumberInput, PasswordInput, RadioSelect, RegexField,
|
NullBooleanField, NumberInput, PasswordInput, RadioSelect, RegexField,
|
||||||
SplitDateTimeField, TextInput, Textarea, TimeField, TypedChoiceField,
|
SplitDateTimeField, TextInput, Textarea, TimeField, TypedChoiceField,
|
||||||
TypedMultipleChoiceField, URLField, ValidationError, Widget,
|
TypedMultipleChoiceField, URLField, UUIDField, ValidationError, Widget,
|
||||||
)
|
)
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.utils import formats
|
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.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.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']))
|
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 os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -294,3 +295,15 @@ if Image:
|
||||||
width_field='headshot_width')
|
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