# 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, 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:
```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": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "django model lifecycle hooks callbacks",
"author": "Robert Singer",
"author_email": "robertgsinger@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/38/d6/5e4ace36af4a5bce03221f232dc9ffd5ff5a04cc4956d305a5c33f51e80e/django-lifecycle-1.2.3.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, when='contents', has_changed=True)\n def on_content_change(self):\n self.updated_at = timezone.now()\n\n @hook(AFTER_UPDATE, when=\"status\", was=\"draft\", is_now=\"published\")\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.3",
"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": "8cd8571ed9b951bfbd2e93a98d35d8d1f7d0d6caea6bc6db1e2a9ddebf5e90c3",
"md5": "9d2d50273b0643814b9613c314b08821",
"sha256": "774ac2e97f506c0599677dda8adcd43e65345e4ab3b45d6ad355c68619b2bc0e"
},
"downloads": -1,
"filename": "django_lifecycle-1.2.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "9d2d50273b0643814b9613c314b08821",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 13887,
"upload_time": "2024-03-01T08:39:23",
"upload_time_iso_8601": "2024-03-01T08:39:23.176899Z",
"url": "https://files.pythonhosted.org/packages/8c/d8/571ed9b951bfbd2e93a98d35d8d1f7d0d6caea6bc6db1e2a9ddebf5e90c3/django_lifecycle-1.2.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "38d65e4ace36af4a5bce03221f232dc9ffd5ff5a04cc4956d305a5c33f51e80e",
"md5": "5a96c42fc04637870ed7d6b89e3caf30",
"sha256": "d3d7e7288894cf6c8c88a01b573a775e15611e419677ef89d59ed56150be21dd"
},
"downloads": -1,
"filename": "django-lifecycle-1.2.3.tar.gz",
"has_sig": false,
"md5_digest": "5a96c42fc04637870ed7d6b89e3caf30",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 11793,
"upload_time": "2024-03-01T08:39:24",
"upload_time_iso_8601": "2024-03-01T08:39:24.584727Z",
"url": "https://files.pythonhosted.org/packages/38/d6/5e4ace36af4a5bce03221f232dc9ffd5ff5a04cc4956d305a5c33f51e80e/django-lifecycle-1.2.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-03-01 08:39:24",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "rsinger86",
"github_project": "django-lifecycle",
"travis_ci": true,
"coveralls": false,
"github_actions": true,
"requirements": [],
"tox": true,
"lcname": "django-lifecycle"
}