mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-04-17 01:40:58 +00:00
convert StatusModifiedField to generic MonitorField, use post_init signal instead of extra DB query
This commit is contained in:
parent
c43b1ba99d
commit
545bccf3ce
4 changed files with 89 additions and 54 deletions
|
|
@ -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 -->')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue