Source code for computedfields.handlers

"""
Module containing the database signal handlers.

The handlers are registered during application startup
in ``apps.ready``.

.. NOTE::

    The handlers are not registered in the managment
    commands ``makemigrations``, ``migrate`` and ``help``.
"""
from .thread_locals import get_DELETES, get_M2M_REMOVE, get_M2M_CLEAR, get_UPDATE_OLD
from django.db import transaction
from .resolver import active_resolver
from .settings import settings
from .signals import resolver_start, resolver_exit

# typing imports
from typing import Iterable, Type, cast
from django.db.models import Model


[docs] def get_old_handler(sender: Type[Model], instance: Model, **kwargs) -> None: """ ``get_old_handler`` handler. ``pre_save`` signal handler to spot incoming fk relation changes. This is needed to correctly update old relations after fk changes, that would contain dirty computed field values after a save. The actual updates on old relations are done during ``post_save``. Skipped during fixtures. """ # do not handle fixtures if kwargs.get('raw'): return # exit early if instance is new # issue #67: also exit on empty pk (cloning does not reset state) if instance._state.adding or not instance.pk: return contributing_fks = active_resolver._fk_map.get(sender) # exit early if model contains no contributing fk fields if not contributing_fks: return candidates = set(contributing_fks) if kwargs.get('update_fields'): candidates &= set(cast(Iterable[str], kwargs.get('update_fields'))) # exit early if no contributing fk field will be updated if not candidates: return # we got an update instance with possibly dirty fk fields # we do simply a full update on all old related fk records for now # FIXME: this might turn out as a major update bottleneck, if so # filter by individual field changes instead? (tests are ~10% slower) data = active_resolver.preupdate_dependent(instance, sender) if data: get_UPDATE_OLD()[instance] = data return
[docs] def postsave_handler(sender: Type[Model], instance: Model, **kwargs) -> None: """ ``post_save`` handler. Directly updates dependent objects. Skipped during fixtures. """ # do not update for fixtures if not kwargs.get('raw'): active_resolver.update_dependent( instance, sender, kwargs.get('update_fields'), old=get_UPDATE_OLD().pop(instance, None), update_local=False, querysize=settings.COMPUTEDFIELDS_QUERYSIZE )
[docs] def predelete_handler(sender: Type[Model], instance: Model, **_) -> None: """ ``pre_delete`` handler. Gets all dependent objects as pk lists and saves them in thread local storage. """ # get the querysets as pk lists to hold them in storage # we have to get pks here since the queryset will be empty after deletion data = active_resolver.preupdate_dependent(instance, sender) if data: get_DELETES()[instance] = data
[docs] def postdelete_handler(sender: Type[Model], instance: Model, **kwargs) -> None: """ ``post_delete`` handler. Loads the dependent objects from the previously saved pk lists and updates them. """ # after deletion we can update the associated computed fields updates = get_DELETES().pop(instance, None) if updates: resolver_start.send(sender=active_resolver) with transaction.atomic(): for model, [pks, fields] in updates.items(): active_resolver.bulk_updater( model._base_manager.filter(pk__in=pks), fields, querysize=settings.COMPUTEDFIELDS_QUERYSIZE ) resolver_exit.send(sender=active_resolver)
# Fix #187, runtime patched M2M through models, that have no entry in the M2M resolver map. def _patch_fields(sender: Type[Model], model: Type[Model], reverse: bool): if not active_resolver.has_computedfields(sender): return for field in model._meta.get_fields(): if field.many_to_many: if not reverse: field = field.remote_field if field.remote_field.through is sender: active_resolver._m2m[sender] = { 'left': field.m2m_field_name(), 'right': field.m2m_reverse_field_name() } return active_resolver._m2m[sender]
[docs] def m2m_handler(sender: Type[Model], instance: Model, **kwargs) -> None: """ ``m2m_change`` handler. Works like the other handlers but on the corresponding m2m actions. """ action = kwargs['action'] reverse = kwargs['reverse'] fields = active_resolver._m2m.get(sender, _patch_fields(sender, kwargs['model'], reverse)) # exit early if we have no update rule for the through model if not fields: return left = fields['right'] if reverse else fields['left'] # fieldname on instance right = fields['left'] if reverse else fields['right'] # fieldname on model if action == 'post_add': active_resolver.update_dependent( sender.objects.filter(**{left: instance.pk, right+'__in': kwargs['pk_set']}), sender, update_local=True, querysize=settings.COMPUTEDFIELDS_QUERYSIZE ) elif action == 'pre_remove': get_M2M_REMOVE()[instance] = active_resolver.preupdate_dependent( sender.objects.filter(**{left: instance.pk, right+'__in': kwargs['pk_set']}) ) elif action == 'post_remove': old = get_M2M_REMOVE().pop(instance, None) if old: resolver_start.send(sender=active_resolver) with transaction.atomic(): for _model, [pks, update_fields] in old.items(): active_resolver.bulk_updater( _model._base_manager.filter(pk__in=pks), update_fields, querysize=settings.COMPUTEDFIELDS_QUERYSIZE ) resolver_exit.send(sender=active_resolver) elif action == 'pre_clear': get_M2M_CLEAR()[instance] = active_resolver.preupdate_dependent( sender.objects.filter(**{left: instance.pk}) ) elif action == 'post_clear': old = get_M2M_CLEAR().pop(instance, None) if old: resolver_start.send(sender=active_resolver) with transaction.atomic(): for _model, [pks, update_fields] in old.items(): active_resolver.bulk_updater( _model._base_manager.filter(pk__in=pks), update_fields, querysize=settings.COMPUTEDFIELDS_QUERYSIZE ) resolver_exit.send(sender=active_resolver)