Commits

Jannis Leidel committed 6860ee8

Added new ``Choices`` class as a replacement for ``ChoiceEnum``. Also renamed Condition* et al to Status*.

Comments (0)

Files changed (5)

model_utils/__init__.py

                            ', '.join(("'%s'" % i[1] for i in self._choices)))
     
 
+class Choices(object):
+    """
+    A class to encapsulate handy functionality for lists of choices
+    for a Django model field.
+
+    Accepts verbose choice names as arguments, and automatically
+    assigns numeric keys to them. When iterated over, behaves as the
+    standard Django choices tuple of two-tuples.
+
+    Attribute access allows conversion of verbose choice name to
+    choice key, dictionary access the reverse.
+
+    Example:
+
+    >>> STATUS = Choices('DRAFT', 'PUBLISHED')
+    >>> STATUS.draft
+    DRAFT
+    >>> STATUS[1]
+    'PUBLISHED'
+    >>> tuple(STATUS)
+    (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED'))
+
+    >>> STATUS = Choices(('DRAFT', 'is a draft'), ('PUBLISHED', 'is published'))
+    >>> STATUS.draft
+    DRAFT
+    >>> tuple(STATUS)
+    (('DRAFT', 'is a draft'), ('PUBLISHED', 'is published'))
+
+    """
+
+    def __init__(self, *choices):
+        self._choices = tuple(self.equalize(choices))
+        self._choice_dict = dict(self._choices)
+        self._reverse_dict = dict(((i[0].lower(), i[0]) for i in self._choices))
+
+    def equalize(self, choices):
+        for choice in choices:
+            if isinstance(choice, (list, tuple)):
+                yield choice
+            else:
+                yield (choice, choice)
+
+    def __iter__(self):
+        return iter(self._choices)
+
+    def __getattr__(self, attname):
+        try:
+            return self._reverse_dict[attname]
+        except KeyError:
+            raise AttributeError(attname)
+
+    def __getitem__(self, key):
+        return self._choices[key][0]
+
+    def __repr__(self):
+        return '%s(%s)' % (self.__class__.__name__,
+                          ', '.join(("'%s'" % i[0] for i in self._choices)))

model_utils/fields.py

 from django.db import models
 from django.conf import settings
 
+from model_utils import Choices
+
 class AutoCreatedField(models.DateTimeField):
     """
     A DateTimeField that automatically populates itself at
         return value
 
 
-def _previous_condition(model_instance, attname, add):
+def _previous_status(model_instance, attname, add):
     if add:
         return None
     pk_value = getattr(model_instance, model_instance._meta.pk.attname)
         return None
     return getattr(current, attname, None)
 
-class ConditionField(models.PositiveIntegerField):
+class StatusField(models.CharField):
     """
-    A PositiveIntegerField that has set conditional choices by default.
+    A CharField that has set status choices by default.
 
     """
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('max_length', 100)
+        super(StatusField, self).__init__(*args, **kwargs)
+
     def contribute_to_class(self, cls, name):
         if not cls._meta.abstract:
-            assert not not hasattr(cls, 'CONDITIONS'), "The model '%s' doesn't have conditions set." % cls.__name__
-            setattr(self, '_choices', cls.CONDITIONS)
-            setattr(self, 'default', tuple(cls.CONDITIONS)[0][0]) # sets first as default
-        super(ConditionField, self).contribute_to_class(cls, name)
+            assert hasattr(cls, 'STATUS'), "The model '%s' doesn't have status choices set." % cls.__name__
+            assert isinstance(cls.STATUS, Choices), "The status choices of model '%s' isn't a subclass of %s" % (cls.__name__, Choices)
+            setattr(self, '_choices', cls.STATUS)
+            setattr(self, 'default', tuple(cls.STATUS)[0][0]) # sets first as default
+        super(StatusField, self).contribute_to_class(cls, name)
 
     def pre_save(self, model_instance, add):
-        previous = _previous_condition(model_instance, 'get_%s_display' % self.attname, add)
+        previous = _previous_status(model_instance, 'get_%s_display' % self.attname, add)
         if previous:
             previous = previous()
-        setattr(model_instance, 'previous_condition', previous)
-        return super(ConditionField, self).pre_save(model_instance, add)
+        setattr(model_instance, 'previous_status', previous)
+        return super(StatusField, self).pre_save(model_instance, add)
 
-class ConditionModifedField(models.DateTimeField):
+class StatusModifedField(models.DateTimeField):
 
     def __init__(self, *args, **kwargs):
         kwargs.setdefault('default', datetime.now)
-        depends_on = kwargs.pop('depends_on', 'condition')
+        depends_on = kwargs.pop('depends_on', 'status')
         if not depends_on:
             raise TypeError(
                 '%s requires a depends_on parameter' % self.__class__.__name__)
         self.depends_on = depends_on
-        super(ConditionModifedField, self).__init__(*args, **kwargs)
+        super(StatusModifedField, self).__init__(*args, **kwargs)
 
     def contribute_to_class(self, cls, name):
-        #print cls._meta, cls
-        assert not getattr(cls._meta, "has_condition_modified_field", False), "A model can't have more than one ConditionModifedField."
-        super(ConditionModifedField, self).contribute_to_class(cls, name)
-        setattr(cls._meta, "has_condition_modified_field", True)
+        assert not getattr(cls._meta, "has_status_modified_field", False), "A model can't have more than one StatusModifedField."
+        super(StatusModifedField, self).contribute_to_class(cls, name)
+        setattr(cls._meta, "has_status_modified_field", True)
 
     def pre_save(self, model_instance, add):
         value = datetime.now()
-        previous = _previous_condition(model_instance, self.depends_on, add)
+        previous = _previous_status(model_instance, self.depends_on, add)
         current = getattr(model_instance, self.depends_on, None)
         if (previous and (previous != current)) or (current and not previous):
             setattr(model_instance, self.attname, value)
-        return super(ConditionModifedField, self).pre_save(model_instance, add)
+        return super(StatusModifedField, self).pre_save(model_instance, add)
 
 
 SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '<!-- split -->')

model_utils/models.py

 
 from model_utils.managers import QueryManager
 from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \
-    ConditionField, ConditionModifedField
+    StatusField, StatusModifedField
 
 class InheritanceCastModel(models.Model):
     """
         except FieldDoesNotExist:
             pass
         cls.add_to_class('timeframed', QueryManager(
-            (models.Q(starts__lte=datetime.now()) | models.Q(starts__isnull=True)) &
-            (models.Q(ends__gte=datetime.now()) | models.Q(ends__isnull=True))
+            (models.Q(start__lte=datetime.now()) | models.Q(start__isnull=True)) &
+            (models.Q(end__gte=datetime.now()) | models.Q(end__isnull=True))
         ))
 
 class TimeFramedModel(models.Model):
     """
-    An abstract base class model that provides ``starts``
-    and ``ends`` fields to record a timeframe.
+    An abstract base class model that provides ``start``
+    and ``end`` fields to record a timeframe.
 
     """
     __metaclass__ = TimeFramedBaseModel
 
-    starts = models.DateTimeField(_('starts'), null=True, blank=True)
-    ends = models.DateTimeField(_('ends'), null=True, blank=True)
+    start = models.DateTimeField(_('start'), null=True, blank=True)
+    end = models.DateTimeField(_('end'), null=True, blank=True)
 
     class Meta:
         abstract = True
 
 
-class ConditionalBaseModel(ModelBase):
+class StatusBaseModel(ModelBase):
     """
-    A model base class for the ``ConditionalModel`` to add
-    a series of model managers for each given condition.
+    A model base class for the ``StatusModel`` to add
+    a series of model managers for each given status.
 
     """
     def _prepare(cls):
-        super(ConditionalBaseModel, cls)._prepare()
-        conditions = getattr(cls, 'CONDITIONS', None)
-        if conditions is None:
+        super(StatusBaseModel, cls)._prepare()
+        status = getattr(cls, 'STATUS', ())
+        if status is None:
             return
-        for value, name in conditions._choices:
+        for value, name in status:
             try:
                 cls._meta.get_field(name)
                 raise ValueError("Model %s has a field named '%s' and "
-                                 "conflicts with a condition."
+                                 "conflicts with a status."
                                  % (cls.__name__, name))
             except FieldDoesNotExist:
                 pass
-            cls.add_to_class(name, QueryManager(**{'condition': value}))
+            cls.add_to_class(value, QueryManager(**{'status': value}))
 
-class ConditionalModel(models.Model):
+class StatusModel(models.Model):
     """
     An abstract base class model that provides self-updating
-    condition fields like ``deleted`` and ``restored``.
+    status fields like ``deleted`` and ``restored``.
 
     """
-    __metaclass__ = ConditionalBaseModel
+    __metaclass__ = StatusBaseModel
 
-    condition = ConditionField(_('condition'))
-    condition_date = ConditionModifedField(_('condition date'))
+    status = StatusField(_('status'))
+    status_date = StatusModifedField(_('status date'))
 
     def __unicode__(self):
-        return self.get_condition_display()
+        return self.get_status_display()
 
     class Meta:
         abstract = True

model_utils/tests/models.py

 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
-from model_utils.models import InheritanceCastModel, TimeStampedModel, ConditionalModel, TimeFramedModel
+from model_utils.models import InheritanceCastModel, TimeStampedModel, StatusModel, TimeFramedModel
 from model_utils.managers import QueryManager
 from model_utils.fields import SplitField
-from model_utils import ChoiceEnum
+from model_utils import Choices
 
 class InheritParent(InheritanceCastModel):
     pass
 class TimeFrame(TimeFramedModel):
     pass
 
-class Condition(ConditionalModel):
-    CONDITIONS = ChoiceEnum(
+class Status(StatusModel):
+    STATUS = Choices(
         ('active', _('active')),
         ('deleted', _('deleted')),
         ('on_hold', _('on hold')),
     )
 
+
 class Post(models.Model):
     published = models.BooleanField()
     confirmed = models.BooleanField()

model_utils/tests/tests.py

 from django.contrib.contenttypes.models import ContentType
 from django.db.models.fields import FieldDoesNotExist
 
-from model_utils import ChoiceEnum
+from model_utils import ChoiceEnum, Choices
 from model_utils.fields import get_excerpt
 from model_utils.tests.models import InheritParent, InheritChild, TimeStamp, \
-    Post, Article, Condition, TimeFrame
+    Post, Article, Status, TimeFrame
 
 
 class GetExcerptTests(TestCase):
     def test_display(self):
         self.assertEquals(self.STATUS.get_deleted_display(), 'DELETED')
 
+class ChoicesTests(TestCase):
+    def setUp(self):
+        self.STATUS = Choices('DRAFT', 'PUBLISHED')
+
+    def test_getattr(self):
+        self.assertEquals(self.STATUS.draft, 'DRAFT')
+
+    def test_getitem(self):
+        self.assertEquals(self.STATUS[1], 'PUBLISHED')
+
+    def test_iteration(self):
+        self.assertEquals(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED')))
+
+    def test_display(self):
+        self.assertEquals(self.STATUS.draft, 'DRAFT')
+
+class LabelChoicesTests(ChoicesTests):
+    def setUp(self):
+        self.STATUS = Choices(
+            ('DRAFT', 'draft'),
+            ('PUBLISHED', 'published'),
+            'DELETED',
+        )
+
+    def test_iteration(self):
+        self.assertEquals(tuple(self.STATUS), (
+            ('DRAFT', 'draft'),
+            ('PUBLISHED', 'published'),
+            ('DELETED', 'DELETED'))
+        )
+
+    def test_display(self):
+        self.assertEquals(self.STATUS.deleted, 'DELETED')
+
 class InheritanceCastModelTests(TestCase):
     def setUp(self):
         self.parent = InheritParent.objects.create()
     def testCreated(self):
         now = datetime.now()
         # objects are out of the timeframe
-        TimeFrame.objects.create(starts=now+timedelta(days=2))
-        TimeFrame.objects.create(ends=now-timedelta(days=1))
+        TimeFrame.objects.create(start=now+timedelta(days=2))
+        TimeFrame.objects.create(end=now-timedelta(days=1))
         self.assertEquals(TimeFrame.timeframed.count(), 0)
 
         # objects in the timeframe for various reasons
-        TimeFrame.objects.create(starts=now-timedelta(days=10))
-        TimeFrame.objects.create(ends=now+timedelta(days=2))
-        TimeFrame.objects.create(starts=now-timedelta(days=1), ends=now+timedelta(days=1))
+        TimeFrame.objects.create(start=now-timedelta(days=10))
+        TimeFrame.objects.create(end=now+timedelta(days=2))
+        TimeFrame.objects.create(start=now-timedelta(days=1), end=now+timedelta(days=1))
         self.assertEquals(TimeFrame.timeframed.count(), 3)
 
 
-class ConditionalModelTests(TestCase):
+class StatusModelTests(TestCase):
+
     def testCreated(self):
-        c1 = Condition.objects.create()
-        c2 = Condition.objects.create()
-        self.assert_(c2.condition_date > c1.condition_date)
-        self.assertEquals(Condition.active.count(), 2)
+        c1 = Status.objects.create()
+        c2 = Status.objects.create()
+        self.assert_(c2.status_date > c1.status_date)
+        self.assertEquals(Status.active.count(), 2)
+        self.assertEquals(Status.deleted.count(), 0)
 
     def testModification(self):
-        t1 = Condition.objects.create()
-        date_created = t1.condition_date
-        t1.condition = t1.CONDITIONS.on_hold
+        t1 = Status.objects.create()
+        date_created = t1.status_date
+        t1.status = t1.STATUS.on_hold
         t1.save()
-        self.assert_(t1.condition_date > date_created)
-        date_changed = t1.condition_date
+        self.assertEquals(Status.active.count(), 0)
+        self.assertEquals(Status.on_hold.count(), 1)
+        self.assert_(t1.status_date > date_created)
+        date_changed = t1.status_date
         t1.save()
-        self.assertEquals(t1.condition_date, date_changed)
-        date_active_again = t1.condition_date
-        t1.condition = t1.CONDITIONS.active
+        self.assertEquals(t1.status_date, date_changed)
+        date_active_again = t1.status_date
+        t1.status = t1.STATUS.active
         t1.save()
-        self.assert_(t1.condition_date > date_active_again)
+        self.assert_(t1.status_date > date_active_again)
 
     def testPreviousConditon(self):
-        c = Condition.objects.create()
-        self.assertEquals(c.previous_condition, None)
-        c.condition = c.CONDITIONS.on_hold
-        c.save()
-        self.assertEquals(c.previous_condition, c.CONDITIONS.get_active_display())
+        status = Status.objects.create()
+        self.assertEquals(status.previous_status, None)
+        status.status = status.STATUS.on_hold
+        status.save()
+        self.assertEquals(status.previous_status, status.STATUS.active)
+
 
 class QueryManagerTests(TestCase):
     def setUp(self):