{"id":13415819,"url":"https://github.com/rsinger86/django-lifecycle","last_synced_at":"2025-04-23T20:55:06.785Z","repository":{"id":30705537,"uuid":"125766600","full_name":"rsinger86/django-lifecycle","owner":"rsinger86","description":"Declarative model lifecycle hooks, an alternative to Signals.","archived":false,"fork":false,"pushed_at":"2025-02-24T10:18:46.000Z","size":1590,"stargazers_count":1372,"open_issues_count":20,"forks_count":96,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-04-23T20:54:38.855Z","etag":null,"topics":["django","django-models","lifecycle-hooks","transition-conditions"],"latest_commit_sha":null,"homepage":"https://rsinger86.github.io/django-lifecycle/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rsinger86.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-18T20:53:33.000Z","updated_at":"2025-04-22T19:13:23.000Z","dependencies_parsed_at":"2023-02-18T12:15:59.412Z","dependency_job_id":"44f28e10-32f3-4bde-8d53-545afa789a63","html_url":"https://github.com/rsinger86/django-lifecycle","commit_stats":{"total_commits":228,"total_committers":39,"mean_commits":5.846153846153846,"dds":0.75,"last_synced_commit":"51b0c45cb33cf50f868c2593dc5cf5daee3324d0"},"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsinger86%2Fdjango-lifecycle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsinger86%2Fdjango-lifecycle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsinger86%2Fdjango-lifecycle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rsinger86%2Fdjango-lifecycle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rsinger86","download_url":"https://codeload.github.com/rsinger86/django-lifecycle/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250514767,"owners_count":21443208,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","django-models","lifecycle-hooks","transition-conditions"],"created_at":"2024-07-30T21:00:52.340Z","updated_at":"2025-04-23T20:55:06.741Z","avatar_url":"https://github.com/rsinger86.png","language":"Python","readme":"# 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\nfrom django_lifecycle.conditions import WhenFieldValueIs, WhenFieldValueWas, WhenFieldHasChanged\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            \u0026 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**: \u003ca href=\"https://rsinger86.github.io/django-lifecycle/\" target=\"_blank\"\u003ehttps://rsinger86.github.io/django-lifecycle\u003c/a\u003e\n\n**Source Code**: \u003ca href=\"https://github.com/rsinger86/django-lifecycle/\" target=\"_blank\"\u003ehttps://github.com/rsinger86/django-lifecycle\u003c/a\u003e\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","funding_links":[],"categories":["Third-Party Packages","Python","Django Utilities"],"sub_categories":["Models"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frsinger86%2Fdjango-lifecycle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frsinger86%2Fdjango-lifecycle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frsinger86%2Fdjango-lifecycle/lists"}