diff --git a/AUTHORS.rst b/AUTHORS.rst index 06a3e01..07e9967 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -50,4 +50,4 @@ | Jack Cushman | Zach Cheung | Daniel Andrlik - +| marfyl \ No newline at end of file diff --git a/docs/fields.rst b/docs/fields.rst index 02ca6ef..1f0d5c5 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 51bde8f..4e04c29 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -55,3 +55,24 @@ 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](https://github.com/jazzband/django-model-utils/blob/master/docs/fields.rst#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 + from model_utils import Choices + + class MyAppModel(UUIDModel): + pass + diff --git a/model_utils/exceptions.py b/model_utils/exceptions.py new file mode 100644 index 0000000..90566cb --- /dev/null +++ b/model_utils/exceptions.py @@ -0,0 +1,2 @@ +class UUIDVersionException(Exception): + pass diff --git a/model_utils/fields.py b/model_utils/fields.py index 2799eac..e0ac7ba 100644 --- a/model_utils/fields.py +++ b/model_utils/fields.py @@ -1,11 +1,14 @@ from __future__ import unicode_literals import django +import uuid from django.db import models from django.conf import settings from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now +from model_utils.exceptions import UUIDVersionException + DEFAULT_CHOICES_NAME = 'STATUS' @@ -17,6 +20,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 +34,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 +58,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 +99,7 @@ class MonitorField(models.DateTimeField): changes. """ + def __init__(self, *args, **kwargs): kwargs.setdefault('default', now) monitor = kwargs.pop('monitor', None) @@ -144,6 +151,7 @@ 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 @@ -252,3 +260,45 @@ 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. Uses Python’s 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 + ------ + UUIDVersionException + UUID version 2 is not supported. + """ + kwargs.setdefault('primary_key', primary_key) + kwargs.setdefault('editable', editable) + + if version == 4: + default = uuid.uuid4 + elif version == 1: + default = uuid.uuid1 + elif version == 2: + raise UUIDVersionException("UUID version 2 is not supported.") + elif version == 3: + default = uuid.uuid3 + elif version == 5: + default = uuid.uuid5 + else: + raise UUIDVersionException("UUID version %s is not valid." % version) + + 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 888aba5..72e48a4 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 @@ -159,8 +165,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: @@ -340,6 +346,7 @@ class StringyDescriptor(object): """ Descriptor that returns a string version of the underlying integer value. """ + def __init__(self, name): self.name = name @@ -393,3 +400,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..74faa4f --- /dev/null +++ b/tests/test_fields/test_uuid_field.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +import uuid + +from django.test import TestCase + +from model_utils.fields import UUIDField +from model_utils.exceptions import UUIDVersionException + + +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(UUIDVersionException, 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) 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)