Examples

Here are some examples to illustrate how you can hook into specific lifecycle moments, optionally based on state transitions.

Specific lifecycle moments

For simple cases, you might always want something to happen at a certain point, such as after saving or before deleting a model instance. When a user is first created, you could process a thumbnail image in the background and send the user an email:

    @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'],
        )

Or you want to email a user when their account is deleted. You could add the decorated method below:

    @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'],
        )

Read on to see how to only fire the hooked method if certain conditions about the model's current and previous state are met.

Transitions between specific values

Maybe you only want the hooked method to run under certain circumstances related to the state of your model. If a model's status field change from "active" to "banned", you may want to send an email to the user:

    @hook(AFTER_UPDATE, when='status', was='active', is_now='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'],
        )

The was and is_now keyword arguments allow you to compare the model's state from when it was first instantiated to the current moment. You can also pass "*" to indicate any value - these are the defaults, meaning that by default the hooked method will fire. The when keyword specifies which field to check against.

Preventing state transitions

You can also enforce certain disallowed transitions. For example, maybe you don't want your staff to be able to delete an active trial because they should expire instead:

    @hook(BEFORE_DELETE, when='has_trial', is_now=True)
    def ensure_trial_not_active(self):
        raise CannotDeleteActiveTrial('Cannot delete trial user!')

We've omitted the was keyword meaning that the initial state of the has_trial field can be any value ("*").

Any change to a field

You can pass the keyword argument has_changed=True to run the hooked method if a field has changed.

    @hook(BEFORE_UPDATE, when='address', has_changed=True)
    def timestamp_address_change(self):
        self.address_updated_at = timezone.now()

When a field's value is NOT

You can have a hooked method fire when a field's value IS NOT equal to a certain value.

    @hook(BEFORE_SAVE, when='email', is_not=None)
    def lowercase_email(self):
        self.email = self.email.lower()

When a field's value was NOT

You can have a hooked method fire when a field's initial value was not equal to a specific value.

    @hook(BEFORE_SAVE, when='status', was_not="rejected", is_now="published")
    def send_publish_alerts(self):
        send_mass_email()

When a field's value changes to

You can have a hooked method fire when a field's initial value was not equal to a specific value but now is.

    @hook(BEFORE_SAVE, when='status', changes_to="published")
    def send_publish_alerts(self):
        send_mass_email()

Generally, changes_to is a shorthand for the situation when was_not and is_now have the same value. The sample above is equal to:

    @hook(BEFORE_SAVE, when='status', was_not="published", is_now="published")
    def send_publish_alerts(self):
        send_mass_email()

Stacking decorators

You can decorate the same method multiple times if you want to hook a method to multiple moments.

    @hook(AFTER_UPDATE, when="published", has_changed=True)
    @hook(AFTER_CREATE, when="type", has_changed=True)
    def handle_update(self):
        # do something

Watching multiple fields

If you want to hook into the same moment, but base its conditions on multiple fields, you can use the when_any parameter.

    @hook(BEFORE_SAVE, when_any=['status', 'type'], has_changed=True)
    def do_something(self):
        # do something

Going deeper with utility methods

If you need to hook into events with more complex conditions, you can take advantage of has_changed and initial_value utility methods:

    @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()