diff --git a/AUTHORS.rst b/AUTHORS.rst index 80d8f44..bd4eef6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -51,4 +51,4 @@ | Jack Cushman | Zach Cheung | Daniel Andrlik - +| marfyl \ No newline at end of file diff --git a/CHANGES.rst b/CHANGES.rst index ff9abf0..dbb6a1a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,7 @@ master (unreleased) - Fix patched `save` in FieldTracker - Upgrades test requirements (pytest, pytest-django, pytest-cov) and skips tox test with Python 3.5 and Django (trunk) +- Add UUIDModel and UUIDField support. 3.1.2 (2018.05.09) ------------------ diff --git a/docs/fields.rst b/docs/fields.rst index 02ca6ef..87c298e 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -154,3 +154,29 @@ If no marker is found in the content, the first two paragraphs (where paragraphs are blocks of text separated by a blank line) are taken to be the excerpt. This number can be customized by setting the ``SPLIT_DEFAULT_PARAGRAPHS`` setting. + + +UUIDField +---------- + +A ``UUIDField`` subclass that provides an UUID field. You can +add this field to any model definition. + +With the param ``primary_key`` you can set if this field is the +primary key for the model, default is True. + +Param ``version`` is an integer that set default UUID version. +Versions 1,3,4 and 5 are supported, default is 4. + +If ``editable`` is set to false the field will not be displayed in the admin +or any other ModelForm, default is False. + + +.. code-block:: python + + from django.db import models + from model_utils.fields import UUIDField + + class MyAppModel(models.Model): + uuid = UUIDField(primary_key=True, version=4, editable=False) + diff --git a/docs/models.rst b/docs/models.rst index 5ddbd08..a2aac4d 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -86,3 +86,26 @@ SoftDeletableModel This abstract base class just provides field ``is_removed`` which is set to True instead of removing the instance. Entities returned in default manager are limited to not-deleted instances. + + +UUIDModel +------------------ + +This abstract base class provides ``id`` field on any model that inherits from it +which will be the primary key. + +If you dont want to set ``id`` as primary key or change the field name, you can be override it +with our `UUIDField`_ + +Also you can override the default uuid version. Versions 1,3,4 and 5 are now supported. + +.. code-block:: python + + from model_utils.models import UUIDModel + + class MyAppModel(UUIDModel): + pass + + + +.. _`UUIDField`: https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#uuidfield diff --git a/model_utils/fields.py b/model_utils/fields.py index 2799eac..701147b 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals import django +import uuid from django.db import models from django.conf import settings +from django.core.exceptions import ValidationError from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now @@ -17,6 +19,7 @@ class AutoCreatedField(models.DateTimeField): By default, sets editable=False, default=datetime.now. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('editable', False) kwargs.setdefault('default', now) @@ -30,6 +33,7 @@ class AutoLastModifiedField(AutoCreatedField): By default, sets editable=False and default=datetime.now. """ + def pre_save(self, model_instance, add): value = now() if not model_instance.pk: @@ -53,6 +57,7 @@ class StatusField(models.CharField): Also features a ``no_check_for_status`` argument to make sure South can handle this field when it freezes a model. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('max_length', 100) self.check_for_status = not kwargs.pop('no_check_for_status', False) @@ -93,6 +98,7 @@ class MonitorField(models.DateTimeField): changes. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('default', now) monitor = kwargs.pop('monitor', None) @@ -144,7 +150,9 @@ SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '') # the number of paragraphs after which to split if no marker SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2) -_excerpt_field_name = lambda name: '_%s_excerpt' % name + +def _excerpt_field_name(name): + return '_%s_excerpt' % name def get_excerpt(content): @@ -252,3 +260,49 @@ class SplitField(models.TextField): name, path, args, kwargs = super(SplitField, self).deconstruct() kwargs['no_excerpt_field'] = True return name, path, args, kwargs + + +class UUIDField(models.UUIDField): + """ + A field for storing universally unique identifiers. Use Python UUID class. + """ + + def __init__(self, primary_key=True, version=4, editable=False, *args, **kwargs): + """ + Parameters + ---------- + primary_key : bool + If True, this field is the primary key for the model. + version : int + An integer that set default UUID version. + editable : bool + If False, the field will not be displayed in the admin or any other ModelForm, + default is false. + + Raises + ------ + ValidationError + UUID version 2 is not supported. + """ + + if version == 2: + raise ValidationError( + 'UUID version 2 is not supported.') + + if version < 1 or version > 5: + raise ValidationError( + 'UUID version is not valid.') + + if version == 1: + default = uuid.uuid1 + elif version == 3: + default = uuid.uuid3 + elif version == 4: + default = uuid.uuid4 + elif version == 5: + default = uuid.uuid5 + + kwargs.setdefault('primary_key', primary_key) + kwargs.setdefault('editable', editable) + kwargs.setdefault('default', default) + super(UUIDField, self).__init__(*args, **kwargs) diff --git a/model_utils/models.py b/model_utils/models.py index 2f21695..96c472b 100644 --- a/model_utils/models.py +++ b/model_utils/models.py @@ -4,16 +4,25 @@ import django from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils.translation import ugettext_lazy as _ + +from model_utils.fields import ( + AutoCreatedField, + AutoLastModifiedField, + StatusField, + MonitorField, + UUIDField, +) +from model_utils.managers import ( + QueryManager, + SoftDeletableManager, +) + if django.VERSION >= (1, 9, 0): from django.db.models.functions import Now now = Now() else: from django.utils.timezone import now -from model_utils.managers import QueryManager, SoftDeletableManager -from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \ - StatusField, MonitorField - class TimeStampedModel(models.Model): """ @@ -135,3 +144,18 @@ class SoftDeletableModel(models.Model): self.save(using=using) else: return super(SoftDeletableModel, self).delete(using=using, *args, **kwargs) + + +class UUIDModel(models.Model): + """ + This abstract base class provides id field on any model that inherits from it + which will be the primary key. + """ + id = UUIDField( + primary_key=True, + version=4, + editable=False, + ) + + class Meta: + abstract = True diff --git a/tests/models.py b/tests/models.py index bab9d4b..6a1f822 100644 --- a/tests/models.py +++ b/tests/models.py @@ -8,7 +8,12 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from model_utils import Choices -from model_utils.fields import SplitField, MonitorField, StatusField +from model_utils.fields import ( + SplitField, + MonitorField, + StatusField, + UUIDField, +) from model_utils.managers import ( QueryManager, InheritanceManager, @@ -19,6 +24,7 @@ from model_utils.models import ( StatusModel, TimeFramedModel, TimeStampedModel, + UUIDModel, ) from tests.fields import MutableField from tests.managers import CustomSoftDeleteManager @@ -169,8 +175,8 @@ class Post(models.Model): objects = models.Manager() public = QueryManager(published=True) - public_confirmed = QueryManager(models.Q(published=True) & - models.Q(confirmed=True)) + public_confirmed = QueryManager( + models.Q(published=True) & models.Q(confirmed=True)) public_reversed = QueryManager(published=True).order_by("-order") class Meta: @@ -369,6 +375,7 @@ class StringyDescriptor(object): """ Descriptor that returns a string version of the underlying integer value. """ + def __init__(self, name): self.name = name @@ -422,3 +429,11 @@ class JoinItemForeignKey(models.Model): on_delete=models.CASCADE ) objects = JoinManager() + + +class CustomUUIDModel(UUIDModel): + pass + + +class CustomNotPrimaryUUIDModel(models.Model): + uuid = UUIDField(primary_key=False) diff --git a/tests/test_fields/test_uuid_field.py b/tests/test_fields/test_uuid_field.py new file mode 100644 index 0000000..3a6c739 --- /dev/null +++ b/tests/test_fields/test_uuid_field.py @@ -0,0 +1,40 @@ +from __future__ import unicode_literals + +import uuid + +from django.core.exceptions import ValidationError +from django.test import TestCase + +from model_utils.fields import UUIDField + + +class UUIDFieldTests(TestCase): + + def test_uuid_version_default(self): + instance = UUIDField() + self.assertEqual(instance.default, uuid.uuid4) + + def test_uuid_version_1(self): + instance = UUIDField(version=1) + self.assertEqual(instance.default, uuid.uuid1) + + def test_uuid_version_2_error(self): + self.assertRaises(ValidationError, UUIDField, 'version', 2) + + def test_uuid_version_3(self): + instance = UUIDField(version=3) + self.assertEqual(instance.default, uuid.uuid3) + + def test_uuid_version_4(self): + instance = UUIDField(version=4) + self.assertEqual(instance.default, uuid.uuid4) + + def test_uuid_version_5(self): + instance = UUIDField(version=5) + self.assertEqual(instance.default, uuid.uuid5) + + def test_uuid_version_bellow_min(self): + self.assertRaises(ValidationError, UUIDField, 'version', 0) + + def test_uuid_version_above_max(self): + self.assertRaises(ValidationError, UUIDField, 'version', 6) diff --git a/tests/test_models/test_uuid_model.py b/tests/test_models/test_uuid_model.py new file mode 100644 index 0000000..5559159 --- /dev/null +++ b/tests/test_models/test_uuid_model.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from tests.models import CustomUUIDModel, CustomNotPrimaryUUIDModel + + +class UUIDFieldTests(TestCase): + + def test_uuid_model_with_uuid_field_as_primary_key(self): + instance = CustomUUIDModel() + instance.save() + self.assertEqual(instance.id.__class__.__name__, 'UUID') + self.assertEqual(instance.id, instance.pk) + + def test_uuid_model_with_uuid_field_as_not_primary_key(self): + instance = CustomNotPrimaryUUIDModel() + instance.save() + self.assertEqual(instance.uuid.__class__.__name__, 'UUID') + self.assertNotEqual(instance.uuid, instance.pk)