### Install django-lifecycle Package Source: https://context7.com/rsinger86/django-lifecycle/llms.txt This command installs the django-lifecycle package using pip. Ensure you have pip installed and configured correctly. This is the first step to using the library in your Django project. ```bash pip install django-lifecycle ``` -------------------------------- ### Stacking multiple decorators for different events - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Decorates a single method to be triggered by multiple lifecycle events with different conditions. This allows for flexible handling of various scenarios using the same function. The example shows handling updates to 'published' and creation updates to 'type'. ```python @hook(AFTER_UPDATE, condition=WhenFieldHasChanged("published", has_changed=True)) @hook(AFTER_CREATE, condition=WhenFieldHasChanged("type", has_changed=True)) def handle_update(self): # do something ``` -------------------------------- ### Install django-lifecycle (Shell) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/index.md Command to install the django-lifecycle package using pip. This package is a dependency for using the provided lifecycle hooks in Django projects. ```shell pip install django-lifecycle ``` -------------------------------- ### Run code after model creation - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Executes a function after a model instance is successfully created. This can be used for background job enqueuing or sending initial notifications. It's triggered immediately after the save operation. ```python @hook(AFTER_CREATE) def do_after_create_jobs(self): enqueue_job(process_thumbnail, self.picture_url) mail.send_mail( 'Welcome!', 'Thank you for joining.', 'from@example.com', ['to@example.com'], ) ``` -------------------------------- ### Run code after model deletion - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Executes a function after a model instance is successfully deleted. This is useful for cleanup tasks or sending final notifications. The hook fires after the delete operation is complete. ```python @hook(AFTER_DELETE) def email_deleted_user(self): mail.send_mail( 'We have deleted your account', 'We will miss you!.', 'customerservice@corporate.com', ['human@gmail.com'], ) ``` -------------------------------- ### Using LifecycleModel base class (Python) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/index.md Example of how to integrate django-lifecycle by inheriting from the abstract base model class LifecycleModel. This approach makes the lifecycle hooks available to the model without requiring additional mixins. ```python from django_lifecycle import LifecycleModel, hook from django.db import models class YourModel(LifecycleModel): name = models.CharField(max_length=50) ``` -------------------------------- ### Lowercase email before save - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Converts the 'email' field to lowercase before saving the model instance, provided the email is not None. This ensures consistent email formatting. The hook runs before the save operation. ```python @hook(BEFORE_SAVE, condition=WhenFieldValueIsNot("email", value=None)) def lowercase_email(self): self.email = self.email.lower() ``` -------------------------------- ### Django Lifecycle Model Utility Methods Example Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/advanced.md Demonstrates the usage of `has_changed` and `initial_value` utility methods on a Django model inheriting from `LifecycleModel`. These methods help track field changes and retrieve initial values, useful for conditional logic within hooks. ```python from django_lifecycle import LifecycleModel, AFTER_UPDATE, hook class UserAccount(LifecycleModel): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) email = models.CharField(max_length=100) marital_status = models.CharField(max_length=100) @hook(AFTER_UPDATE) def on_name_change_heck_on_marital_status(self): if ( self.has_changed('last_name') and not self.has_changed('marital_status') ): send_mail( to=self.email, "Has your marital status changed recently?" ) ``` -------------------------------- ### Run code after creation on commit - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Executes a function after a model instance is created and the transaction is committed. This is ideal for tasks that rely on database persistence, such as sending notifications that require the object to be fully saved. The `on_commit=True` argument ensures execution within the transaction. ```python @hook(AFTER_CREATE, on_commit=True) def do_after_create_jobs(self): enqueue_job(send_item_shipped_notication, self.item_id) ``` -------------------------------- ### Conditional Field Updates with has_changed and initial_value (Python) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Demonstrates using `has_changed` to check if specific fields have been modified during an update and `initial_value` to access a field's state before the update. This allows for granular control over when certain actions are triggered based on data changes. ```python @hook(AFTER_UPDATE) def on_update(self): if self.has_changed('username') and not self.has_changed('password'): # do the thing here if self.initial_value('login_attempts') == 2: do_thing() else: do_other_thing() ``` -------------------------------- ### Send publish alerts when status changes to published (shorthand) - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Sends mass email alerts when a model's 'status' field changes specifically to 'published'. This is a shorthand for checking if the status was not 'published' and is now 'published'. The hook runs before the save operation. ```python @hook(BEFORE_SAVE, condition=WhenFieldValueChangesTo("status", value="published")) def send_publish_alerts(self): send_mass_email() ``` -------------------------------- ### Timestamp address change on update - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Updates a 'address_updated_at' timestamp field whenever the 'address' field of a model changes during an update. It uses `WhenFieldHasChanged` to detect modifications to the specified field. The hook runs before the update is saved. ```python @hook(BEFORE_UPDATE, condition=WhenFieldHasChanged("address", has_changed=True)) def timestamp_address_change(self): self.address_updated_at = timezone.now() ``` -------------------------------- ### Send publish alerts when status changes to published - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Sends mass email alerts when a model's 'status' field changes from any value other than 'rejected' to 'published'. This is useful for notifying relevant parties about content becoming live. The hook runs before the save operation. ```python @hook( BEFORE_SAVE, condition=( WhenFieldValueWasNot("status", value="rejected") & WhenFieldValueIs("status", value="published") ) ) def send_publish_alerts(self): send_mass_email() ``` -------------------------------- ### Conditional state transition email - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Triggers an email notification when a model's 'status' field changes from 'active' to 'banned' during an update. It utilizes `WhenFieldValueWas` and `WhenFieldValueIs` conditions to precisely target the state transition. The hook runs after the update is saved. ```python @hook( AFTER_UPDATE, condition=( WhenFieldValueWas("status", value="active") & WhenFieldValueIs('status', value='banned') ) ) def email_banned_user(self): mail.send_mail( 'You have been banned', 'You may or may not deserve it.', 'communitystandards@corporate.com', ['mr.troll@hotmail.com'], ) ``` -------------------------------- ### Django Lifecycle Custom Chainable Conditions Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/advanced.md Illustrates creating custom, chainable conditions by inheriting from `ChainableCondition` in django-lifecycle. This allows combining multiple conditions using logical operators (`&`). The example defines an `IsNedFlanders` condition and uses it with `WhenFieldHasChanged` in a hook. ```python from django_lifecycle import BEFORE_SAVE from django_lifecycle.conditions import WhenFieldHasChanged from django_lifecycle.conditions.base import ChainableCondition class IsNedFlanders(ChainableCondition): def __call__(self, instance, update_fields=None): return ( instance.first_name == "Ned" and instance.last_name == "Flanders" ) @hook( BEFORE_SAVE, condition=( WhenFieldHasChanged("first_name") & WhenFieldHasChanged("last_name") & IsNedFlanders() ) ) def foo(): ... ``` -------------------------------- ### Prevent deletion of active trial user - Python Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/examples.md Prevents the deletion of a model instance if its 'has_trial' field is True. This is enforced before the delete operation begins. It raises a custom exception `CannotDeleteActiveTrial` to signal the invalid transition. The hook runs before the delete operation. ```python @hook(BEFORE_DELETE, condition=WhenFieldValueIs("has_trial", value=True)) def ensure_trial_not_active(self): raise CannotDeleteActiveTrial('Cannot delete trial user!') ``` -------------------------------- ### Django Lifecycle Chaining Conditions with @hook Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/hooks_and_conditions.md Illustrates how to chain multiple conditions using the `&` operator with the @hook decorator in Django Lifecycle. This example shows a hook that triggers only when both 'first_name' changes to 'Ned' and 'last_name' changes to 'Flanders' before an update. ```python from django_lifecycle.conditions import WhenFieldValueChangesTo from django_lifecycle import hook, BEFORE_UPDATE class YourModel: # ... other model fields and methods @hook( BEFORE_UPDATE, condition=( WhenFieldValueChangesTo("first_name", value="Ned") & WhenFieldValueChangesTo("last_name", value="Flanders") ) ) def do_something(self): print("First name changed to Ned and last name changed to Flanders.") # Your custom logic here pass ``` -------------------------------- ### Django Lifecycle Custom Condition Functions Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/advanced.md Shows how to create custom condition functions for django-lifecycle hooks. These functions accept the model instance and optional `update_fields` and return a boolean to determine if the hook should execute. Example demonstrates checking for specific field changes and values. ```python def changes_to_ned_flanders(instance, update_fields=None) -> bool: return ( instance.has_changed("first_name") and instance.has_changed("last_name") and instance.first_name == "Ned" and instance.last_name == "Flanders" ) ``` -------------------------------- ### Watch Foreign Key Field Changes with Django Lifecycle Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Monitor changes in fields on related models using dot notation in Django. This example demonstrates how to trigger hooks based on changes in a foreign key's fields, such as 'company.name' or 'company.status'. It requires django_lifecycle, Django, and Django's mail module. ```python from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE from django.db import models from django.core import mail class Company(LifecycleModel): name = models.CharField(max_length=200) status = models.CharField(max_length=20, default='active') class Project(LifecycleModel): title = models.CharField(max_length=200) company = models.ForeignKey(Company, on_delete=models.CASCADE) manager_email = models.EmailField() @hook(AFTER_UPDATE, when="company.name", has_changed=True) def notify_company_name_change(self): mail.send_mail( 'Company Name Changed', f'The company for project {self.title} is now named {self.company.name}', 'from@example.com', [self.manager_email], ) @hook( AFTER_UPDATE, when="company.status", was="active", is_now="inactive" ) def handle_company_deactivation(self): print(f'Company for project {self.title} became inactive') # Usage company = Company.objects.create(name='Acme Corp', status='active') project = Project.objects.create( title='Website Redesign', company=company, manager_email='manager@example.com' ) company.name = 'Acme Industries' company.save() project.save() # Triggers notify_company_name_change (company.name changed) company.status = 'inactive' company.save() project.save() # Triggers handle_company_deactivation ``` -------------------------------- ### Bypass Lifecycle Hooks with skip_hooks Parameter Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Explains how to use the skip_hooks parameter when saving a model instance to prevent any associated lifecycle hooks (defined using the @hook decorator) from executing. This is useful in scenarios where you need to perform a save operation without triggering any custom logic, such as during bulk data imports or specific administrative tasks. The example shows a model with BEFORE_SAVE and AFTER_SAVE hooks that are bypassed. ```python from django_lifecycle import LifecycleModel, hook, BEFORE_SAVE, AFTER_SAVE from django.db import models class AuditLog(LifecycleModel): message = models.TextField() processed = models.BooleanField(default=False) processed_count = models.IntegerField(default=False) @hook(BEFORE_SAVE) def increment_counter(self): self.processed_count += 1 print(f'Processing count: {self.processed_count}') @hook(AFTER_SAVE) def send_notification(self): print(f'Notification sent for: {self.message}') # Usage log = AuditLog.objects.create(message='User login') # To skip hooks: # log.skip_hooks = True # log.message = 'Admin action' # log.save() # increment_counter and send_notification will NOT be called ``` -------------------------------- ### Using LifecycleModelMixin (Python) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/index.md Demonstrates integrating django-lifecycle by using the LifecycleModelMixin with a standard Django model. This allows a model to gain lifecycle hook capabilities without inheriting from a specific base model class. ```python from django.db import models from django_lifecycle import LifecycleModelMixin, hook class YourModel(LifecycleModelMixin, models.Model): name = models.CharField(max_length=50) ``` -------------------------------- ### Alternative Model save() and __init__ (Python) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/index.md Illustrates a traditional Django approach to handling model lifecycle events by overriding the save() and __init__() methods. This method involves manual tracking of field changes and can lead to less readable code compared to using lifecycle hooks. ```python from django.db import models from django.utils import timezone from django.contrib.auth.models import User as AuthUser class Article(models.Model): contents = models.TextField() updated_at = models.DateTimeField(null=True) status = models.CharField(max_length=20, choices=[('draft', 'Draft'), ('published', 'Published')]) editor = models.ForeignKey(AuthUser, on_delete=models.CASCADE) 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: # Assuming send_email is a defined function # send_email(self.editor.email, "An article has published!") pass ``` -------------------------------- ### Define Django Model with Lifecycle Hooks Source: https://github.com/rsinger86/django-lifecycle/blob/master/README.md Demonstrates how to define a Django model inheriting from LifecycleModel and apply @hook decorators for pre-update and post-update events. It uses conditional logic based on field value changes or specific values to trigger these hooks. Dependencies include django_lifecycle and its condition utilities. ```python from django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE, AFTER_UPDATE from django_lifecycle.conditions import WhenFieldValueIs, WhenFieldValueWas, WhenFieldHasChanged from django.db import models from django.utils import timezone # Assuming AuthUser and send_email are defined elsewhere # from django.contrib.auth.models import User as AuthUser # def send_email(email, message): # pass class Article(LifecycleModel): contents = models.TextField() updated_at = models.DateTimeField(null=True) status = models.CharField(max_length=20, choices=[('draft', 'Draft'), ('published', 'Published')]) # Assuming CharField with choices for status editor = models.ForeignKey('auth.User', on_delete=models.CASCADE) # Example ForeignKey @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!") pass # Placeholder for email sending ``` -------------------------------- ### Add django_lifecycle_checks to INSTALLED_APPS (Python) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/index.md Configuration snippet for Django projects to include 'django_lifecycle_checks' in the INSTALLED_APPS setting. This optional step enables checks that ensure the mixin or base model is correctly applied to models using django-lifecycle. ```python INSTALLED_APPS = [ # ... "django_lifecycle_checks", # ... ] ``` -------------------------------- ### Define Lifecycle Hooks with LifecycleModel Base Class Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Demonstrates how to use the LifecycleModel base class to add lifecycle hooks to a Django model. It shows hooks for updating a timestamp when a password changes (BEFORE_UPDATE) and sending a welcome email after a user is created (AFTER_CREATE). ```python from django_lifecycle import LifecycleModel, hook, AFTER_CREATE, BEFORE_UPDATE from django.db import models from django.core import mail from django.utils import timezone class UserAccount(LifecycleModel): username = models.CharField(max_length=100) email = models.EmailField(null=True) joined_at = models.DateTimeField(null=True) password = models.CharField(max_length=200) password_updated_at = models.DateTimeField(null=True) @hook(BEFORE_UPDATE, when="password", has_changed=True) def timestamp_password_change(self): self.password_updated_at = timezone.now() @hook(AFTER_CREATE) def send_welcome_email(self): mail.send_mail( 'Welcome!', 'Thank you for joining.', 'from@example.com', [self.email], ) # Usage user = UserAccount.objects.create( username='john_doe', email='john@example.com', password='hashed_password' ) # Triggers AFTER_CREATE hook, sends welcome email # joined_at is automatically set user.password = 'new_hashed_password' user.save() # Triggers BEFORE_UPDATE hook, updates password_updated_at ``` -------------------------------- ### Django Lifecycle @hook Decorator Signature Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/hooks_and_conditions.md Demonstrates the signature of the @hook decorator from the django-lifecycle library. It outlines the parameters for specifying lifecycle moments, conditions, priority, and commit behavior, including legacy parameters. ```python from typing import Optional, List, Any from django_lifecycle import hook, DEFAULT_PRIORITY from django_lifecycle import types @hook( moment: str, condition: Optional[types.Condition] = None, priority: int = DEFAULT_PRIORITY, on_commit: Optional[bool] = None, # Legacy parameters when: str = None, when_any: List[str] = None, has_changed: bool = None, is_now: Any = '*', is_not: Any = None, was: Any = '*', was_not: Any = None, changes_to: Any = None, ) def model_method(self): pass ``` -------------------------------- ### Create Custom Chainable Conditions for Hooks Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Illustrates creating custom, chainable conditions like `IsValidEmail` and `SalaryIncreasedByMoreThan` that can be combined with built-in conditions for fine-grained hook triggering based on model instance state and changes. ```python from django_lifecycle import LifecycleModel, hook, BEFORE_SAVE from django_lifecycle.conditions import WhenFieldHasChanged from django_lifecycle.conditions.base import ChainableCondition from django.db import models class IsValidEmail(ChainableCondition): def __call__(self, instance, update_fields=None): if instance.email: return '@' in instance.email and '.' in instance.email return False class SalaryIncreasedByMoreThan(ChainableCondition): def __init__(self, percentage): self.percentage = percentage def __call__(self, instance, update_fields=None): if instance.has_changed('salary'): old_salary = instance.initial_value('salary') new_salary = instance.salary if old_salary and old_salary > 0: increase_pct = ((new_salary - old_salary) / old_salary) * 100 return increase_pct > self.percentage return False class StaffMember(LifecycleModel): name = models.CharField(max_length=100) email = models.EmailField() salary = models.DecimalField(max_digits=10, decimal_places=2) @hook( BEFORE_SAVE, condition=( WhenFieldHasChanged("email", has_changed=True) & IsValidEmail() ) ) def normalize_valid_email(self): self.email = self.email.lower().strip() @hook( BEFORE_SAVE, condition=SalaryIncreasedByMoreThan(percentage=10) ) def flag_large_salary_increase(self): print(f'Large salary increase detected for {self.name}') # Usage staff = StaffMember.objects.create( name='Alice', email='ALICE@EXAMPLE.COM', salary=50000 ) staff.email = ' BOB@TEST.COM ' staff.save() staff.salary = 60000 staff.save() staff.salary = 65000 staff.save() ``` -------------------------------- ### Control Hook Execution Order with Priority Parameter Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Explains how to manage the sequence of hook executions using the `priority` parameter in the `@hook` decorator. Lower priority numbers indicate earlier execution, allowing for controlled multi-step processes. ```python from django_lifecycle import LifecycleModel, hook, BEFORE_SAVE from django.db import models class Product(LifecycleModel): name = models.CharField(max_length=200) price = models.DecimalField(max_digits=10, decimal_places=2) tax = models.DecimalField(max_digits=10, decimal_places=2, default=0) final_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) @hook(BEFORE_SAVE, priority=1) def calculate_tax(self): self.tax = self.price * 0.10 print(f'Step 1: Tax calculated: ${self.tax}') @hook(BEFORE_SAVE, priority=2) def calculate_final_price(self): self.final_price = self.price + self.tax print(f'Step 2: Final price: ${self.final_price}') @hook(BEFORE_SAVE, priority=3) def round_final_price(self): self.final_price = round(self.final_price, 2) print(f'Step 3: Rounded final price: ${self.final_price}') # Usage product = Product.objects.create(name='Widget', price=100.00) ``` -------------------------------- ### Control Hook Execution with skip_hooks Parameter Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Demonstrates how to skip hook execution during model save operations using the `skip_hooks=True` argument. This is useful for bulk updates or when side effects need to be avoided. ```python log.processed = True log.save() log.message = 'Updated message' log.save(skip_hooks=True) for log_entry in AuditLog.objects.filter(processed=False): log_entry.processed = True log_entry.save(skip_hooks=True) ``` -------------------------------- ### Chaining Conditions with & and | in Django Lifecycle Hooks Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Demonstrates how to combine multiple conditions for Django lifecycle hooks using the logical AND (&) and OR (|) operators. This allows for more granular control over when a hook is triggered based on field values, previous values, or changes. It requires importing specific condition classes like WhenFieldValueIs and WhenFieldHasChanged. ```python from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE from django_lifecycle.conditions import ( WhenFieldValueIs, WhenFieldValueWas, WhenFieldHasChanged ) from django.db import models class Payment(LifecycleModel): amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=20, default='pending') payment_method = models.CharField(max_length=20) @hook( AFTER_UPDATE, condition=( WhenFieldValueWas("status", value="pending") & WhenFieldValueIs("status", value="completed") & WhenFieldValueIs("payment_method", value="credit_card") ) ) def process_credit_card_completion(self): print(f'Credit card payment of ${self.amount} completed') @hook( AFTER_UPDATE, condition=( (WhenFieldValueIs("status", value="failed") | WhenFieldValueIs("status", value="cancelled")) & WhenFieldHasChanged("status", has_changed=True) ) ) def handle_payment_failure_or_cancellation(self): print(f'Payment ended in {self.status} state') @hook( AFTER_UPDATE, condition=( WhenFieldHasChanged("amount", has_changed=True) | WhenFieldHasChanged("payment_method", has_changed=True) ) ) def log_payment_modification(self): print('Payment details modified') # Usage payment = Payment.objects.create( amount=100.00, status='pending', payment_method='credit_card' ) payment.status = 'completed' payment.save() # Triggers process_credit_card_completion (all 3 conditions met) payment.status = 'failed' payment.save() # Triggers handle_payment_failure_or_cancellation (OR condition) payment.amount = 150.00 payment.save() # Triggers log_payment_modification (amount changed) ``` -------------------------------- ### Conditional Hooks with Legacy when() and when_any() Parameters Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Illustrates how to use legacy 'when' and 'when_any' parameters within the hook decorator to conditionally trigger lifecycle hooks based on field changes. These parameters allow specifying conditions like 'is_not', 'has_changed', or comparing old and new values. This approach is still supported for backward compatibility and provides flexible control over hook execution. ```python from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE, BEFORE_SAVE from django.db import models from django.core import mail class Contact(LifecycleModel): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) email = models.EmailField() phone = models.CharField(max_length=20) @hook(BEFORE_SAVE, when="email", is_not=None) def lowercase_email(self): self.email = self.email.lower() @hook(AFTER_UPDATE, when="email", has_changed=True) def notify_email_change(self): old_email = self.initial_value('email') mail.send_mail( 'Email Updated', f'Your email changed from {old_email} to {self.email}', 'from@example.com', [self.email], ) @hook(AFTER_UPDATE, when_any=["first_name", "last_name"], has_changed=True) def notify_name_change(self): # Fires if either first_name OR last_name changed mail.send_mail( 'Name Updated', 'Your name has been updated in our system', 'from@example.com', [self.email], ) @hook(AFTER_UPDATE, when="phone", was="*", is_now=None) def handle_phone_removed(self): # Fires when phone changes from any value to None print('Phone number was removed') # Usage contact = Contact.objects.create( first_name='Jane', last_name='Smith', email='JANE@EXAMPLE.COM', phone='555-1234' ) # email lowercased to 'jane@example.com' contact.email = 'NEWEMAIL@EXAMPLE.COM' contact.save() # Triggers notify_email_change contact.first_name = 'Janet' contact.save() # Triggers notify_name_change (first_name changed) contact.last_name = 'Johnson' contact.save() # Triggers notify_name_change (last_name changed) contact.phone = None contact.save() # Triggers handle_phone_removed ``` -------------------------------- ### Add Lifecycle Hooks with LifecycleModelMixin Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Illustrates using LifecycleModelMixin to add lifecycle hooks to an existing Django model without altering its inheritance. It includes hooks for updating a timestamp when content changes (BEFORE_SAVE) and sending an email upon publishing an article (AFTER_UPDATE with conditions). ```python from django.db import models from django_lifecycle import LifecycleModelMixin, hook, AFTER_UPDATE, BEFORE_SAVE from django_lifecycle.conditions import WhenFieldValueIs, WhenFieldValueWas class Article(LifecycleModelMixin, models.Model): title = models.CharField(max_length=200) contents = models.TextField() updated_at = models.DateTimeField(null=True) status = models.CharField( max_length=20, choices=[('draft', 'Draft'), ('published', 'Published')], default='draft' ) editor = models.ForeignKey('auth.User', on_delete=models.CASCADE) @hook( AFTER_UPDATE, condition=( WhenFieldValueWas("status", value="draft") & WhenFieldValueIs("status", value="published") ) ) def on_publish(self): mail.send_mail( 'Article Published', f'Your article "{self.title}" has been published!', 'from@example.com', [self.editor.email], ) @hook(BEFORE_SAVE, when="contents", has_changed=True) def timestamp_content_update(self): self.updated_at = timezone.now() # Usage article = Article.objects.create( title='My Article', contents='Original content', editor=some_user ) article.contents = 'Updated content' article.save() # Triggers BEFORE_SAVE hook, updates updated_at article.status = 'published' article.save() # Triggers AFTER_UPDATE hook with condition check # Sends email because status changed from draft to published ``` -------------------------------- ### Stack Multiple Django Lifecycle Hooks on Same Method Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Apply multiple lifecycle hooks to a single method for different events in Django. This demonstrates using multiple @hook decorators on one method to execute it for various conditions, such as creation or specific field updates. Dependencies include django_lifecycle and Django models. ```python from django_lifecycle import LifecycleModel, hook, AFTER_CREATE, AFTER_UPDATE from django_lifecycle.conditions import WhenFieldHasChanged from django.db import models class Article(LifecycleModel): title = models.CharField(max_length=200) view_count = models.IntegerField(default=0) published = models.BooleanField(default=False) @hook(AFTER_CREATE) @hook(AFTER_UPDATE, condition=WhenFieldHasChanged("published", has_changed=True)) def update_search_index(self): # Runs on creation AND when published field changes print(f'Updating search index for: {self.title}') @hook(AFTER_CREATE) @hook(AFTER_UPDATE, condition=WhenFieldHasChanged("title", has_changed=True)) def generate_slug(self): # Runs on creation AND when title changes print(f'Generating slug from: {self.title}') # Usage article = Article.objects.create(title='My Article') # Triggers both update_search_index and generate_slug article.published = True article.save() # Triggers update_search_index only article.title = 'My Updated Article' article.save() # Triggers generate_slug only ``` -------------------------------- ### Django Lifecycle: WhenFieldValueIs and WhenFieldValueWas Conditions Source: https://context7.com/rsinger86/django-lifecycle/llms.txt These conditions allow hooks to trigger based on the current and initial values of a model field. WhenFieldValueWas checks the value before an update, and WhenFieldValueIs checks the value after an update. They are useful for detecting specific state transitions, such as from 'trial' to 'active' or 'active' to 'cancelled'. ```python from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE from django_lifecycle.conditions import WhenFieldValueIs, WhenFieldValueWas from django.db import models from django.core import mail class Subscription(LifecycleModel): user_email = models.EmailField() status = models.CharField(max_length=20, default='trial') @hook( AFTER_UPDATE, condition=( WhenFieldValueWas("status", value="trial") & WhenFieldValueIs("status", value="active") ) ) def on_trial_to_active(self): mail.send_mail( 'Subscription Activated', 'Your subscription is now active!', 'from@example.com', [self.user_email], ) @hook( AFTER_UPDATE, condition=( WhenFieldValueWas("status", value="active") & WhenFieldValueIs("status", value="cancelled") ) ) def on_active_to_cancelled(self): mail.send_mail( 'Subscription Cancelled', 'We\'re sorry to see you go.', 'from@example.com', [self.user_email], ) @hook( AFTER_UPDATE, condition=WhenFieldValueIs("status", value="expired") ) def on_any_to_expired(self): # Fires when status becomes 'expired' from any previous value mail.send_mail( 'Subscription Expired', 'Your subscription has expired.', 'from@example.com', [self.user_email], ) # Usage sub = Subscription.objects.create(user_email='user@example.com', status='trial') sub.status = 'active' sub.save() # Triggers on_trial_to_active hook, sends activation email sub.status = 'cancelled' sub.save() # Triggers on_active_to_cancelled hook, sends cancellation email sub.status = 'expired' sub.save() # Triggers on_any_to_expired hook ``` -------------------------------- ### Watch ForeignKey Field Value Changes (Django) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/fk_changes.md This hook fires when a field on a related foreign key model changes to a specific value, using dot-notation. It requires `django-lifecycle` and the `WhenFieldValueChangesTo` condition. Performance warnings apply: use `.select_related()` to avoid N+1 query issues. ```python from django.db import models from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE, WhenFieldValueChangesTo from django.core import mail class Organization(models.Model): name = models.CharField(max_length=100) class UserAccount(LifecycleModel): username = models.CharField(max_length=100) email = models.CharField(max_length=600) employer = models.ForeignKey(Organization, on_delete=models.SET_NULL) @hook(AFTER_UPDATE, condition=WhenFieldValueChangesTo("employer.name", value="Google")) def notify_user_of_google_buy_out(self): mail.send_mail("Update", "Google bought your employer!", ["to@example.com"]) ``` -------------------------------- ### Define Django Model with Lifecycle Hooks (Python) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/index.md Demonstrates defining a Django model that inherits from LifecycleModel and uses the @hook decorator to define lifecycle methods. These hooks can be triggered before or after model events like updates, and can include conditions based on field changes or values. ```python from django_lifecycle import LifecycleModel, hook, BEFORE_UPDATE, AFTER_UPDATE from django.db import models from django.utils import timezone from django.contrib.auth.models import User as AuthUser from django_lifecycle.conditions import WhenFieldHasChanged, WhenFieldValueWas, WhenFieldValueIs class Article(LifecycleModel): contents = models.TextField() updated_at = models.DateTimeField(null=True) status = models.CharField(max_length=20, choices=[('draft', 'Draft'), ('published', 'Published')]) editor = models.ForeignKey(AuthUser, on_delete=models.CASCADE) @hook( BEFORE_UPDATE, condition=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): # Assuming send_email is a defined function # send_email(self.editor.email, "An article has published!") pass ``` -------------------------------- ### Traditional Django Model Save with Manual State Tracking Source: https://github.com/rsinger86/django-lifecycle/blob/master/README.md Illustrates the conventional Django approach to handling model state changes within the save method and __init__. This method involves manually storing original field values to detect changes and trigger actions like updating timestamps or sending emails. It's presented as a less readable alternative to the @hook decorator. ```python from django.db import models from django.utils import timezone # Assuming AuthUser and send_email are defined elsewhere # from django.contrib.auth.models import User as AuthUser # def send_email(email, message): # pass class ArticleLegacy(models.Model): contents = models.TextField() updated_at = models.DateTimeField(null=True) status = models.CharField(max_length=20, choices=[('draft', 'Draft'), ('published', 'Published')]) editor = models.ForeignKey('auth.User', on_delete=models.CASCADE) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Store original values to detect changes in save self._orig_contents = self.contents self._orig_status = self.status def save(self, *args, **kwargs): # Handle content change before saving if self.pk is not None and self.contents != self._orig_contents: self.updated_at = timezone.now() # Call the parent save method super().save(*args, **kwargs) # Handle status change after saving if self.status != self._orig_status: # send_email(self.editor.email, "An article has published!") pass # Placeholder for email sending ``` -------------------------------- ### Django @hook Decorator with Lifecycle Constants Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Attaches methods to specific lifecycle moments (e.g., BEFORE_CREATE, AFTER_SAVE) of a Django model. It allows defining actions before or after various operations like creation, update, save, and deletion. Dependencies include django_lifecycle and Django's ORM. ```python from django_lifecycle import ( LifecycleModel, hook, BEFORE_CREATE, AFTER_CREATE, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_SAVE, AFTER_SAVE, BEFORE_DELETE, AFTER_DELETE ) from django.db import models from django.utils import timezone class Order(LifecycleModel): order_number = models.CharField(max_length=50) status = models.CharField(max_length=20, default='pending') created_at = models.DateTimeField(null=True) deleted_at = models.DateTimeField(null=True) @hook(BEFORE_CREATE) def set_order_number(self): self.order_number = f'ORD-{timezone.now().timestamp()}' @hook(AFTER_CREATE) def log_creation(self): print(f'Order {self.order_number} created') @hook(BEFORE_UPDATE) def validate_update(self): if self.status == 'cancelled': raise ValueError('Cannot update cancelled order') @hook(AFTER_UPDATE) def log_update(self): print(f'Order {self.order_number} updated') @hook(BEFORE_SAVE) def normalize_data(self): self.status = self.status.lower() @hook(AFTER_SAVE) def sync_to_external_system(self): # Call external API pass @hook(BEFORE_DELETE) def soft_delete_timestamp(self): self.deleted_at = timezone.now() @hook(AFTER_DELETE) def cleanup_related_data(self): print(f'Order {self.order_number} deleted') # Usage order = Order.objects.create() # Triggers BEFORE_CREATE, AFTER_CREATE, BEFORE_SAVE, AFTER_SAVE # order_number is auto-generated, logs creation order.status = 'shipped' order.save() # Triggers BEFORE_UPDATE, AFTER_UPDATE, BEFORE_SAVE, AFTER_SAVE order.delete() # Triggers BEFORE_DELETE, AFTER_DELETE ``` -------------------------------- ### Watch ForeignKey Reference Changes (Django) Source: https://github.com/rsinger86/django-lifecycle/blob/master/docs/fk_changes.md This hook fires when the foreign key reference itself changes in the database. It requires the `django-lifecycle` library and the `WhenFieldHasChanged` condition. The input is the model instance being updated, and the output is a notification sent via email. ```python from django.db import models from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE, WhenFieldHasChanged from django.core import mail class Organization(models.Model): name = models.CharField(max_length=100) class UserAccount(LifecycleModel): username = models.CharField(max_length=100) email = models.CharField(max_length=600) employer = models.ForeignKey(Organization, on_delete=models.SET_NULL) @hook(AFTER_UPDATE, condition=WhenFieldHasChanged("employer", has_changed=True)) def notify_user_of_employer_change(self): mail.send_mail("Update", "You now work for someone else!", [self.email]) ``` -------------------------------- ### Track Field Changes and Initial Values with has_changed() and initial_value() Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Demonstrates how to use has_changed() to check if a field's value has been modified and initial_value() to retrieve the field's value before the current save operation. This is useful for implementing complex conditional logic within lifecycle hooks, such as sending notifications or logging changes. It requires the LifecycleModel base class and hook decorator. ```python from django_lifecycle import LifecycleModel, hook, AFTER_UPDATE from django.db import models from django.core import mail class Employee(LifecycleModel): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) email = models.EmailField() department = models.CharField(max_length=100) salary = models.DecimalField(max_digits=10, decimal_places=2) @hook(AFTER_UPDATE) def check_name_and_department_change(self): if self.has_changed('first_name') or self.has_changed('last_name'): if not self.has_changed('email'): # Name changed but email didn't - might need update mail.send_mail( 'Email Verification', 'Your name changed. Is your email still correct?', 'hr@example.com', [self.email], ) if self.has_changed('department'): old_dept = self.initial_value('department') new_dept = self.department print(f'Employee moved from {old_dept} to {new_dept}') @hook(AFTER_UPDATE) def check_salary_change(self): if self.has_changed('salary'): old_salary = self.initial_value('salary') new_salary = self.salary if new_salary > old_salary: increase = new_salary - old_salary print(f'Salary increased by ${increase}') else: decrease = old_salary - new_salary print(f'Salary decreased by ${decrease}') # Usage emp = Employee.objects.create( first_name='John', last_name='Doe', email='john@example.com', department='Engineering', salary=75000 ) emp.first_name = 'Jonathan' emp.save() # has_changed('first_name') returns True # Sends email verification emp.department = 'Management' emp.salary = 85000 emp.save() # initial_value('department') returns 'Engineering' # initial_value('salary') returns 75000 # Logs: "Employee moved from Engineering to Management" # Logs: "Salary increased by $10000" ``` -------------------------------- ### Django Lifecycle: WhenFieldValueIsNot and WhenFieldValueWasNot Conditions Source: https://context7.com/rsinger86/django-lifecycle/llms.txt These conditions allow hooks to trigger when a field's value is not equal to a specified value. WhenFieldValueIsNot checks the current value, while WhenFieldValueWasNot checks the value before an update. They are useful for conditional logic, such as only processing an update if a field's status is not 'rejected' before being 'published'. ```python from django_lifecycle import LifecycleModel, hook, BEFORE_SAVE, AFTER_UPDATE from django_lifecycle.conditions import WhenFieldValueIsNot, WhenFieldValueWasNot from django.db import models class BlogPost(LifecycleModel): title = models.CharField(max_length=200) content = models.TextField() email = models.EmailField(null=True) status = models.CharField(max_length=20, default='draft') @hook(BEFORE_SAVE, condition=WhenFieldValueIsNot("email", value=None)) def lowercase_email(self): # Only runs when email is not None self.email = self.email.lower() @hook( AFTER_UPDATE, condition=( WhenFieldValueWasNot("status", value="rejected") & WhenFieldValueIs("status", value="published") ) ) def send_publish_alerts(self): # Only sends alerts if we're publishing from a non-rejected state # (e.g., from draft or pending, but not from rejected) print(f'Publishing post: {self.title}') # Usage post = BlogPost.objects.create( title='My Post', content='Content here', email='ADMIN@EXAMPLE.COM' ) # email is lowercased to 'admin@example.com' post.status = 'published' post.save() # Triggers send_publish_alerts (was draft, not rejected) post.status = 'rejected' post.save() post.status = 'published' post.save() # Does NOT trigger send_publish_alerts (was rejected) ``` -------------------------------- ### Defer Django Lifecycle Hook Execution Until Commit with on_commit=True Source: https://context7.com/rsinger86/django-lifecycle/llms.txt Explains the use of the `on_commit=True` parameter in Django lifecycle hooks to defer execution until the database transaction has successfully committed. This is crucial for actions like sending emails or making external API calls, ensuring they only occur if the database operation is finalized, preventing race conditions or sending notifications for rolled-back transactions. It requires Django's transaction management. ```python from django_lifecycle import LifecycleModel, hook, AFTER_CREATE, AFTER_UPDATE from django.db import models from django.core import mail class Order(LifecycleModel): order_number = models.CharField(max_length=50) customer_email = models.EmailField() status = models.CharField(max_length=20, default='pending') @hook(AFTER_CREATE, on_commit=True) def send_confirmation_email(self): # Only sends email after transaction commits # Ensures order exists in database before external notification mail.send_mail( 'Order Confirmation', f'Your order {self.order_number} has been received', 'from@example.com', [self.customer_email], ) @hook(AFTER_UPDATE, when="status", has_changed=True, on_commit=True) def notify_status_change(self): # Ensures status update is committed before notification mail.send_mail( 'Order Status Update', f'Your order is now {self.status}', 'from@example.com', [self.customer_email], ) # Usage with transaction from django.db import transaction try: with transaction.atomic(): order = Order.objects.create( order_number='ORD123', customer_email='customer@example.com' ) # send_confirmation_email NOT called yet # If exception occurs here, no email is sent order.status = 'confirmed' order.save() # notify_status_change NOT called yet # Transaction committed successfully # NOW both hooks execute: send_confirmation_email and notify_status_change except Exception as e: # Transaction rolled back, no hooks fire pass ```