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

This commit is contained in:
Carl Meyer 2010-04-15 23:47:28 -04:00
parent c43b1ba99d
commit 545bccf3ce
4 changed files with 89 additions and 54 deletions

View file

@ -32,20 +32,14 @@ class AutoLastModifiedField(AutoCreatedField):
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):
@ -55,36 +49,49 @@ class StatusField(models.CharField):
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 -->')

View file

@ -7,7 +7,7 @@ from django.db.models.fields import FieldDoesNotExist
from model_utils.managers import QueryManager
from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \
StatusField, StatusModifiedField
StatusField, MonitorField
class InheritanceCastModel(models.Model):
"""
@ -60,8 +60,8 @@ class TimeFramedModel(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(
@ -74,28 +74,29 @@ class TimeFramedModel(models.Model):
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

View file

@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
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):
@ -18,6 +18,10 @@ class TimeStamp(TimeStampedModel):
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')),

View file

@ -8,7 +8,7 @@ from django.db.models.fields import FieldDoesNotExist
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):
@ -76,6 +76,28 @@ class SplitFieldTests(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')
@ -86,6 +108,7 @@ class ChoicesTests(TestCase):
def test_iteration(self):
self.assertEquals(tuple(self.STATUS), (('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED')))
class LabelChoicesTests(ChoicesTests):
def setUp(self):
self.STATUS = Choices(
@ -159,25 +182,25 @@ class StatusModelTests(TestCase):
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):
@ -186,19 +209,19 @@ 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):