This project provides a @hook
decorator as well as a base model and mixin to add lifecycle hooks to your Django models. Django's built-in approach to offering lifecycle hooks is Signals. However, my team often finds that Signals introduce unnecessary indirection and are at odds with Django's "fat models" approach.
Django Lifecycle Hooks supports:
- Python 3.8, 3.9, 3.10, 3.11, and 3.12
- Django 3.2, 4.0, 4.1, 4.2, and 5.0
In short, you can write model code like this:
from django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE, AFTER_UPDATE
class Article(LifecycleModel):
contents = models.TextField()
updated_at = models.DateTimeField(null=True)
status = models.ChoiceField(choices=['draft', 'published'])
editor = models.ForeignKey(AuthUser)
@hook(BEFORE_UPDATE, when='contents', has_changed=True)
def on_content_change(self):
self.updated_at = timezone.now()
@hook(AFTER_UPDATE, when="status", was="draft", is_now="published")
def on_publish(self):
send_email(self.editor.email, "An article has published!")
Instead of overriding save
and __init__
in a clunky way that hurts readability:
# same class and field declarations as above ...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._orig_contents = self.contents
self._orig_status = self.status
def save(self, *args, **kwargs):
if self.pk is not None and self.contents != self._orig_contents:
self.updated_at = timezone.now()
super().save(*args, **kwargs)
if self.status != self._orig_status:
send_email(self.editor.email, "An article has published!")
Documentation: https://rsinger86.github.io/django-lifecycle
Source Code: https://github.com/rsinger86/django-lifecycle
- Drop support for Python < 3.8.
- Drop support for Django < 3.2.
- Confirm support for Django 5.0.
- Correct package info to note that Django 4.0, 4.1, and 4.2 are supported.
- Initial state gets reset using
transaction.on_commit()
, fixing thehas_changed()
andinitial_value()
methods for on_commit hooks. Thanks @alb3rto269!
- Drops Python 3.6 support
- Adds
priority
hook kwarg to control the order in which hooked methods fire. Thanks @EnriqueSoria! - Internal cleanup/refactoring. Thanks @EnriqueSoria!
- Adds missing
packaging
toinstall_requires
. Thanks @mikedep333!
- Makes the
has_changed
,changes_to
conditions depend on whether the field in question was included in the SQL update/insert statement by checking theupdate_fields
argument passed to save.
- Adds optional @hook
on_commit
argument for executing hooks when the database transaction is committed. Thanks @amcclosky!
- Correct packge info to note that Django 3.2 is supported.
- Run hooked methods inside transactions, just as signals do. Thanks @amirmotlagh!
- Makes hooks work with OneToOneFields. Thanks @bahmdev!
- Prevents calling a hooked method twice with the same state. Thanks @garyd203!
- Added missing return to
delete()
method override. Thanks @oaosman84!
- Significant performance improvements. Thanks @dralley!
- Fixes issue with
GenericForeignKey
. Thanks @bmbouter!
- Updates to use constants for hook names; updates docs to indicate Python 3.8/Django 3.x support. Thanks @thejoeejoee!
- Adds static typed variables for hook names; thanks @Faisal-Manzer!
- Fixes some typos in docs; thanks @tomdyson and @bmispelon!
- Fixes bug in
utils._get_field_names
that could cause recursion bug in some cases.
- Adds
changes_to
condition - thanks @samitnuk! Also some typo fixes in docs.
- Remove variable type annotation for Python 3.5 compatability.
- Adds
when_any
hook parameter to watch multiple fields for state changes
- Adds
was_not
condition - Allow watching changes to FK model field values, not just FK references
- Fixes missing README.md issue that broke install.
- Fixes urlman-compatability.
- Fixes
initial_value(field_name)
behavior - should return value even if no change. Thanks @adamJLev!
- Fixes bug preventing hooks from firing for custom PKs. Thanks @atugushev!
- Fixes m2m field bug, in which accessing auto-generated reverse field in
before_create
causes exception b/c PK does not exist yet. Thanks @garyd203!
- Resets model's comparison state for hook conditions after
save
called.
- Fixed support for adding multiple
@hook
decorators to same method.
- Removes residual mixin methods from earlier implementation.
- Save method now accepts
skip_hooks
, an optional boolean keyword argument that controls whether hooked methods are called.
- Fixed bug in
_potentially_hooked_methods
that caused unwanted side effects by accessing model instance methods decorated with@cache_property
or@property
.
- Added Django 1.8 support. Thanks @jtiai!
- Tox testing added for Python 3.4, 3.5, 3.6 and Django 1.8, 1.11 and 2.0. Thanks @jtiai!
Tests are found in a simplified Django project in the /tests
folder. Install the project requirements and do ./manage.py test
to run them.
See License.