Commits

Carl Meyer  committed f3c2602

convert StatusModifiedField to generic MonitorField, use post_init signal instead of extra DB query

  • Participants
  • Parent commits de0465f

Comments (0)

Files changed (4)

File model_utils/fields.py

         return value
 
 
-def _previous_status(model_instance, attname, add):
-    if add:
-        return None
-    pk_value = getattr(model_instance, model_instance._meta.pk.attname)
-    try:
-        current = model_instance.__class__._default_manager.get(pk=pk_value)
-    except model_instance.__class__.DoesNotExist:
-        return None
-    return getattr(current, attname, None)
-
-
 class StatusField(models.CharField):
     """
-    A CharField that has set status choices by default.
+    A CharField that looks for a ``STATUS`` class-attribute and
+    automatically uses that as ``choices``. The first option in
+    ``STATUS`` is set as the default.
+
+    Also has a default max_length so you don't have to worry about
+    setting that.
 
     """
     def __init__(self, *args, **kwargs):
     def contribute_to_class(self, cls, name):
         if not cls._meta.abstract:
             assert hasattr(cls, 'STATUS'), \
-                "To use StatusField, the model '%s' must have a STATUS choices attribute." \
+                "To use StatusField, the model '%s' must have a STATUS choices class attribute." \
                 % cls.__name__
             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)
 
 
-class StatusModifiedField(models.DateTimeField):
-
+class MonitorField(models.DateTimeField):
+    """
+    A DateTimeField that monitors another field on the same model and
+    sets itself to the current date/time whenever the monitored field
+    changes.
+    
+    """
     def __init__(self, *args, **kwargs):
         kwargs.setdefault('default', datetime.now)
-        depends_on = kwargs.pop('depends_on', 'status')
-        if not depends_on:
+        monitor = kwargs.pop('monitor', None)
+        if not monitor:
             raise TypeError(
-                '%s requires a depends_on parameter' % self.__class__.__name__)
-        self.depends_on = depends_on
-        super(StatusModifiedField, self).__init__(*args, **kwargs)
+                '%s requires a "monitor" argument' % self.__class__.__name__)
+        self.monitor = monitor
+        super(MonitorField, self).__init__(*args, **kwargs)
 
     def contribute_to_class(self, cls, name):
-        assert not getattr(cls._meta, "has_status_modified_field", False), "A model can't have more than one StatusModifiedField."
-        super(StatusModifiedField, self).contribute_to_class(cls, name)
-        setattr(cls._meta, "has_status_modified_field", True)
+        self.monitor_attname = '_monitor_%s' % name
+        models.signals.post_init.connect(self._save_initial, sender=cls)
+        super(MonitorField, self).contribute_to_class(cls, name)
+
+    def get_monitored_value(self, instance):
+        return getattr(instance, self.monitor)
+    
+    def _save_initial(self, sender, instance, **kwargs):
+        setattr(instance, self.monitor_attname,
+                self.get_monitored_value(instance))
 
     def pre_save(self, model_instance, add):
         value = datetime.now()
-        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):
+        previous = getattr(model_instance, self.monitor_attname, None)
+        current = self.get_monitored_value(model_instance)
+        if previous != current:
             setattr(model_instance, self.attname, value)
-        return super(StatusModifiedField, self).pre_save(model_instance, add)
+            self._save_initial(model_instance.__class__, model_instance)
+        return super(MonitorField, self).pre_save(model_instance, add)
 
 
 SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '<!-- split -->')

File model_utils/models.py

 
 from model_utils.managers import QueryManager
 from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \
-    StatusField, StatusModifiedField
+    StatusField, MonitorField
 
 class InheritanceCastModel(models.Model):
     """
         super(TimeFramedModel, self).__init__(*args, **kwargs)
         try:
             self._meta.get_field('timeframed')
-            raise ValueError("Model %s has a field named 'timeframed' and "
-                             "conflicts with a manager." % self.__name__)
+            raise ValueError("Model '%s' has a field named 'timeframed' which "
+                             "conflicts with the TimeFramedModel manager." % self.__name__)
         except FieldDoesNotExist:
             pass
         self.__class__.add_to_class('timeframed', QueryManager(
 
 class StatusModel(models.Model):
     """
-    An abstract base class model that provides self-updating
-    status fields like ``deleted`` and ``restored``.
+    An abstract base class model with a ``status`` field that
+    automatically uses a ``STATUS`` class attribute of choices, a
+    ``status_changed`` date-time field that records when ``status``
+    was last modified, and an automatically-added manager for each
+    status that returns objects with that status only.
 
     """
     status = StatusField(_('status'))
-    status_date = StatusModifiedField(_('status date'))
+    status_changed = MonitorField(_('status changed'), monitor='status')
 
     def __init__(self, *args, **kwargs):
         super(StatusModel, self).__init__(*args, **kwargs)
         for value, name in getattr(self, 'STATUS', ()):
             try:
                 self._meta.get_field(name)
-                raise ValueError("Model %s has a field named '%s' and "
-                                 "conflicts with a status."
-                                 % (self.__name__, name))
+                from django.core.exceptions import ImproperlyConfigured
+                raise ImproperlyConfigured("StatusModel: Model '%s' has a field named '%s' which "
+                                           "conflicts with a status of the same name."
+                                           % (self.__name__, name))
             except FieldDoesNotExist:
                 pass
             self.__class__.add_to_class(value, QueryManager(status=value))
 
-    def __unicode__(self):
-        return self.get_status_display()
-
     class Meta:
         abstract = True
 

File model_utils/tests/models.py

 
 from model_utils.models import InheritanceCastModel, TimeStampedModel, StatusModel, TimeFramedModel
 from model_utils.managers import QueryManager
-from model_utils.fields import SplitField
+from model_utils.fields import SplitField, MonitorField
 from model_utils import Choices
 
 class InheritParent(InheritanceCastModel):
 class TimeFrame(TimeFramedModel):
     pass
 
+class Monitored(models.Model):
+    name = models.CharField(max_length=25)
+    name_changed = MonitorField(monitor='name')
+
 class Status(StatusModel):
     STATUS = Choices(
         ('active', _('active')),

File model_utils/tests/tests.py

 from model_utils import ChoiceEnum, Choices
 from model_utils.fields import get_excerpt
 from model_utils.tests.models import InheritParent, InheritChild, TimeStamp, \
-    Post, Article, Status, Status2, TimeFrame
+    Post, Article, Status, Status2, TimeFrame, Monitored
 
 
 class GetExcerptTests(TestCase):
         self.assertRaises(AttributeError, _invalid_assignment)
 
 
+class MonitorFieldTests(TestCase):
+    def setUp(self):
+        self.instance = Monitored(name='Charlie')
+        self.created = self.instance.name_changed
+
+    def test_save_no_change(self):
+        self.instance.save()
+        self.assertEquals(self.instance.name_changed, self.created)
+
+    def test_save_changed(self):
+        self.instance.name = 'Maria'
+        self.instance.save()
+        self.failUnless(self.instance.name_changed > self.created)
+
+    def test_double_save(self):
+        self.instance.name = 'Jose'
+        self.instance.save()
+        changed = self.instance.name_changed
+        self.instance.save()
+        self.assertEquals(self.instance.name_changed, changed)
+
+        
 class ChoicesTests(TestCase):
     def setUp(self):
         self.STATUS = Choices('DRAFT', 'PUBLISHED')
     def test_iteration(self):
         self.assertEquals(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED')))
 
+        
 class LabelChoicesTests(ChoicesTests):
     def setUp(self):
         self.STATUS = Choices(
     def testCreated(self):
         c1 = self.model.objects.create()
         c2 = self.model.objects.create()
-        self.assert_(c2.status_date > c1.status_date)
+        self.assert_(c2.status_changed > c1.status_changed)
         self.assertEquals(self.model.active.count(), 2)
         self.assertEquals(self.model.deleted.count(), 0)
 
     def testModification(self):
         t1 = self.model.objects.create()
-        date_created = t1.status_date
+        date_created = t1.status_changed
         t1.status = t1.STATUS.on_hold
         t1.save()
         self.assertEquals(self.model.active.count(), 0)
         self.assertEquals(self.model.on_hold.count(), 1)
-        self.assert_(t1.status_date > date_created)
-        date_changed = t1.status_date
+        self.assert_(t1.status_changed > date_created)
+        date_changed = t1.status_changed
         t1.save()
-        self.assertEquals(t1.status_date, date_changed)
-        date_active_again = t1.status_date
+        self.assertEquals(t1.status_changed, date_changed)
+        date_active_again = t1.status_changed
         t1.status = t1.STATUS.active
         t1.save()
-        self.assert_(t1.status_date > date_active_again)
+        self.assert_(t1.status_changed > date_active_again)
 
         
 class Status2ModelTests(StatusModelTests):
 
     def testModification(self):
         t1 = self.model.objects.create()
-        date_created = t1.status_date
+        date_created = t1.status_changed
         t1.status = t1.STATUS[2][0] # boring on_hold status
         t1.save()
         self.assertEquals(self.model.active.count(), 0)
         self.assertEquals(self.model.on_hold.count(), 1)
-        self.assert_(t1.status_date > date_created)
-        date_changed = t1.status_date
+        self.assert_(t1.status_changed > date_created)
+        date_changed = t1.status_changed
         t1.save()
-        self.assertEquals(t1.status_date, date_changed)
-        date_active_again = t1.status_date
+        self.assertEquals(t1.status_changed, date_changed)
+        date_active_again = t1.status_changed
         t1.status = t1.STATUS[0][0] # boring active status
         t1.save()
-        self.assert_(t1.status_date > date_active_again)
+        self.assert_(t1.status_changed > date_active_again)
 
 
 class QueryManagerTests(TestCase):