# Django Lifecycle Hooks
[![Package version](https://badge.fury.io/py/django-lifecycle.svg)](https://pypi.python.org/pypi/django-lifecycle)
[![Python versions](https://img.shields.io/pypi/status/django-lifecycle.svg)](https://img.shields.io/pypi/status/django-lifecycle.svg/)
[![Python versions](https://img.shields.io/pypi/pyversions/django-lifecycle.svg)](https://pypi.org/project/django-lifecycle/)
![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-lifecycle)
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](https://docs.djangoproject.com/en/dev/topics/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.7, 3.8, 3.9, 3.10, 3.11, and 3.12
* Django 2.2, 3.2, 4.0, 4.1, 4.2, and 5.0
In short, you can write model code like this:
```python
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, WhenFieldHasChanged("contents", has_changed=True))
def on_content_change(self):
self.updated_at = timezone.now()
@hook(
AFTER_UPDATE,
condition=(
WhenFieldValueWas("status", value="draft")
& WhenFieldValueIs("status", value="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:
```python
# 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**: <a href="https://rsinger86.github.io/django-lifecycle/" target="_blank">https://rsinger86.github.io/django-lifecycle</a>
**Source Code**: <a href="https://github.com/rsinger86/django-lifecycle/" target="_blank">https://github.com/rsinger86/django-lifecycle</a>
---
# Changelog
See [Changelog](CHANGELOG.md)
# Testing
Tests are found in a simplified Django project in the `/tests` folder. Install the project requirements and do `./manage.py test` to run them.
# License
See [License](LICENSE.md).
Raw data
{
"_id": null,
"home_page": "https://github.com/rsinger86/django-lifecycle",
"name": "django-lifecycle",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "django model lifecycle hooks callbacks",
"author": "Robert Singer",
"author_email": "robertgsinger@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/9c/ac/4e5ce8cb8b2eef726157bbfb6f227e79a4bf8e14d6346b21490b689464fb/django_lifecycle-1.2.4.tar.gz",
"platform": null,
"description": "# Django Lifecycle Hooks\n\n[![Package version](https://badge.fury.io/py/django-lifecycle.svg)](https://pypi.python.org/pypi/django-lifecycle)\n[![Python versions](https://img.shields.io/pypi/status/django-lifecycle.svg)](https://img.shields.io/pypi/status/django-lifecycle.svg/)\n[![Python versions](https://img.shields.io/pypi/pyversions/django-lifecycle.svg)](https://pypi.org/project/django-lifecycle/)\n![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-lifecycle)\n\nThis 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](https://docs.djangoproject.com/en/dev/topics/signals/). However, my team often finds that Signals introduce unnecessary indirection and are at odds with Django's \"fat models\" approach.\n\n**Django Lifecycle Hooks** supports:\n\n* Python 3.7, 3.8, 3.9, 3.10, 3.11, and 3.12\n* Django 2.2, 3.2, 4.0, 4.1, 4.2, and 5.0\n\nIn short, you can write model code like this:\n\n```python\nfrom django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE, AFTER_UPDATE\n\n\nclass Article(LifecycleModel):\n contents = models.TextField()\n updated_at = models.DateTimeField(null=True)\n status = models.ChoiceField(choices=['draft', 'published'])\n editor = models.ForeignKey(AuthUser)\n\n @hook(BEFORE_UPDATE, WhenFieldHasChanged(\"contents\", has_changed=True))\n def on_content_change(self):\n self.updated_at = timezone.now()\n\n @hook(\n AFTER_UPDATE, \n condition=(\n WhenFieldValueWas(\"status\", value=\"draft\")\n & WhenFieldValueIs(\"status\", value=\"published\")\n )\n )\n def on_publish(self):\n send_email(self.editor.email, \"An article has published!\")\n```\n\nInstead of overriding `save` and `__init__` in a clunky way that hurts readability:\n\n```python\n # same class and field declarations as above ...\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self._orig_contents = self.contents\n self._orig_status = self.status\n\n\n def save(self, *args, **kwargs):\n if self.pk is not None and self.contents != self._orig_contents:\n self.updated_at = timezone.now()\n\n super().save(*args, **kwargs)\n\n if self.status != self._orig_status:\n send_email(self.editor.email, \"An article has published!\")\n```\n\n---\n\n**Documentation**: <a href=\"https://rsinger86.github.io/django-lifecycle/\" target=\"_blank\">https://rsinger86.github.io/django-lifecycle</a>\n\n**Source Code**: <a href=\"https://github.com/rsinger86/django-lifecycle/\" target=\"_blank\">https://github.com/rsinger86/django-lifecycle</a>\n\n---\n\n# Changelog\n\nSee [Changelog](CHANGELOG.md)\n\n# Testing\n\nTests are found in a simplified Django project in the `/tests` folder. Install the project requirements and do `./manage.py test` to run them.\n\n# License\n\nSee [License](LICENSE.md).\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Declarative model lifecycle hooks.",
"version": "1.2.4",
"project_urls": {
"Documentation": "https://rsinger86.github.io/django-lifecycle/",
"Homepage": "https://github.com/rsinger86/django-lifecycle",
"Source": "https://github.com/rsinger86/django-lifecycle"
},
"split_keywords": [
"django",
"model",
"lifecycle",
"hooks",
"callbacks"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "ab3185dd0766c135e8688d88849fc3d245c783cab435cedfb6d9041a911ee22f",
"md5": "e59bda2a6bc49bb2552ad4a6045c114d",
"sha256": "b54aea17b50de45adb5c90a06eea0171afa0d547682f51990dffb578b82fc658"
},
"downloads": -1,
"filename": "django_lifecycle-1.2.4-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e59bda2a6bc49bb2552ad4a6045c114d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 15012,
"upload_time": "2024-06-07T10:40:31",
"upload_time_iso_8601": "2024-06-07T10:40:31.822140Z",
"url": "https://files.pythonhosted.org/packages/ab/31/85dd0766c135e8688d88849fc3d245c783cab435cedfb6d9041a911ee22f/django_lifecycle-1.2.4-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "9cac4e5ce8cb8b2eef726157bbfb6f227e79a4bf8e14d6346b21490b689464fb",
"md5": "3a4745c83f278ac8edb5421e80f8a674",
"sha256": "b37add8a95d0e85f9f97e652fac989cd5914cddb2380d933b6568f80238ab61e"
},
"downloads": -1,
"filename": "django_lifecycle-1.2.4.tar.gz",
"has_sig": false,
"md5_digest": "3a4745c83f278ac8edb5421e80f8a674",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 12223,
"upload_time": "2024-06-07T10:40:33",
"upload_time_iso_8601": "2024-06-07T10:40:33.678910Z",
"url": "https://files.pythonhosted.org/packages/9c/ac/4e5ce8cb8b2eef726157bbfb6f227e79a4bf8e14d6346b21490b689464fb/django_lifecycle-1.2.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-06-07 10:40:33",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "rsinger86",
"github_project": "django-lifecycle",
"travis_ci": true,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "asgiref",
"specs": [
[
"==",
"3.4.1"
]
]
},
{
"name": "Click",
"specs": [
[
"==",
"7.0"
]
]
},
{
"name": "Django",
"specs": [
[
"==",
"5.0b1"
]
]
},
{
"name": "django-capture-on-commit-callbacks",
"specs": [
[
"==",
"1.10.0"
]
]
},
{
"name": "djangorestframework",
"specs": [
[
"==",
"3.11.2"
]
]
},
{
"name": "ghp-import",
"specs": [
[
"==",
"2.0.2"
]
]
},
{
"name": "importlib-metadata",
"specs": [
[
"==",
"4.8.1"
]
]
},
{
"name": "Jinja2",
"specs": [
[
"==",
"2.11.3"
]
]
},
{
"name": "livereload",
"specs": [
[
"==",
"2.6.1"
]
]
},
{
"name": "Markdown",
"specs": [
[
"==",
"3.2.1"
]
]
},
{
"name": "MarkupSafe",
"specs": [
[
"==",
"1.1.1"
]
]
},
{
"name": "mergedeep",
"specs": [
[
"==",
"1.3.4"
]
]
},
{
"name": "mkdocs",
"specs": [
[
"==",
"1.2.3"
]
]
},
{
"name": "mkdocs-material",
"specs": [
[
"==",
"4.6.3"
]
]
},
{
"name": "packaging",
"specs": [
[
"==",
"21.0"
]
]
},
{
"name": "Pygments",
"specs": [
[
"==",
"2.7.4"
]
]
},
{
"name": "pymdown-extensions",
"specs": [
[
"==",
"6.3"
]
]
},
{
"name": "pyparsing",
"specs": [
[
"==",
"3.0.0"
]
]
},
{
"name": "python-dateutil",
"specs": [
[
"==",
"2.8.2"
]
]
},
{
"name": "pytz",
"specs": [
[
"==",
"2023.3"
]
]
},
{
"name": "PyYAML",
"specs": [
[
"==",
"6.0.1"
]
]
},
{
"name": "pyyaml-env-tag",
"specs": [
[
"==",
"0.1"
]
]
},
{
"name": "six",
"specs": [
[
"==",
"1.14.0"
]
]
},
{
"name": "sqlparse",
"specs": [
[
"==",
"0.3.0"
]
]
},
{
"name": "tornado",
"specs": [
[
"==",
"6.0.3"
]
]
},
{
"name": "urlman",
"specs": [
[
"==",
"1.2.0"
]
]
},
{
"name": "watchdog",
"specs": [
[
"==",
"2.1.6"
]
]
},
{
"name": "zipp",
"specs": [
[
"==",
"3.6.0"
]
]
}
],
"tox": true,
"lcname": "django-lifecycle"
}