def m2m_changed_signal(sender, instance, action, reverse, model, pk_set, using, **kwargs) -> None: """ Django sends this signal when many-to-many relationships change. One of the more complex signals, due to the fact that change can be reversed we need to either process instance field changes of pk_set (reverse=False) or pk_set field changes of instance. (reverse=True) The changes will always get applied in the model where the field in defined. # TODO: post_remove also gets triggered when there is nothing actually getting removed :return: None """ if action not in ['post_add', 'post_remove', 'pre_clear', 'post_clear']: return if action == 'pre_clear': operation = Operation.DELETE return pre_clear_processor( sender, instance, list(pk_set) if pk_set else None, model, reverse, operation, ) elif action == 'post_add': operation = Operation.CREATE elif action == 'post_clear': operation = Operation.DELETE else: operation = Operation.DELETE targets = model.objects.filter(pk__in=list(pk_set)) if pk_set else [] if reverse: for target in [ t for t in targets if not lazy_model_exclusion(t, operation, t.__class__) ]: post_processor(sender, target, target.__class__, operation, [instance]) else: if lazy_model_exclusion(instance, operation, instance.__class__): return post_processor(sender, instance, instance.__class__, operation, targets)
def post_delete_signal(sender, instance, **kwargs) -> None: """ Signal is getting called after instance deletion. We just redirect the event to the post_processor. TODO: consider doing a "delete snapshot" :param sender: model class :param instance: model instance :param kwargs: required bt django :return: - """ get_or_create_meta(instance) instance._meta.dal.event = None if lazy_model_exclusion(instance, Operation.DELETE, instance.__class__): return post_processor(Operation.DELETE, sender, instance)
def post_save_signal(sender, instance, created, update_fields: frozenset, **kwargs) -> None: """ Signal is getting called after a save has been concluded. When this is the case we can be sure the save was successful and then only propagate the changes to the handler. :param sender: model class :param instance: model instance :param created: bool, was the model created? :param update_fields: which fields got explicitly updated? :param kwargs: django needs kwargs to be there :return: - """ status = Operation.CREATE if created else Operation.MODIFY if lazy_model_exclusion( instance, status, instance.__class__, ): return get_or_create_meta(instance) suffix = f'' if (status == Operation.MODIFY and hasattr(instance._meta.dal, 'modifications') and settings.model.detailed_message): suffix = ( f' | Modifications: ' f'{", ".join([m.short() for m in instance._meta.dal.modifications])}' ) if update_fields is not None and hasattr(instance._meta.dal, 'modifications'): instance._meta.dal.modifications = [ m for m in instance._meta.dal.modifications if m.field.name in update_fields ] post_processor(status, sender, instance, update_fields, suffix)
def pre_save_signal(sender, instance, **kwargs) -> None: """ Compares the current instance and old instance (fetched via the pk) and generates a dictionary of changes :param sender: :param instance: :param kwargs: :return: None """ get_or_create_meta(instance) # clear the event to be sure instance._meta.dal.event = None operation = Operation.MODIFY try: pre = sender.objects.get(pk=instance.pk) except ObjectDoesNotExist: # __dict__ is used on pre, therefore we need to create a function # that uses __dict__ too, but returns nothing. pre = lambda _: None operation = Operation.CREATE excluded = lazy_model_exclusion(instance, operation, instance.__class__) if excluded: return old, new = pre.__dict__, instance.__dict__ previously = set(k for k in old.keys() if not k.startswith('_') and old[k] is not None) currently = set(k for k in new.keys() if not k.startswith('_') and new[k] is not None) added = currently.difference(previously) deleted = previously.difference(currently) changed = ( set(k for k, v in set( # generate a set from the dict of old values # exclude protected and magic attributes (k, v) for k, v in old.items() if not k.startswith('_') ).difference( set( # generate a set from the dict of new values # also exclude the protected and magic attributes (k, v) for k, v in new.items() if not k.startswith('_'))) # the difference between the two sets # because the keys are the same, but values are different # will result in a change set of changed values # ('a', 'b') in old and new will get eliminated # ('a', 'b') in old and ('a', 'c') in new will # result in ('a', 'b') in the new set as that is # different. ) # remove all added and deleted attributes from the changelist # they would still be present, because None -> Value .difference(added).difference(deleted)) summary = [ *({ 'operation': Operation.CREATE, 'previous': None, 'current': new[k], 'key': k, } for k in added), *({ 'operation': Operation.DELETE, 'previous': old[k], 'current': None, 'key': k, } for k in deleted), *({ 'operation': Operation.MODIFY, 'previous': old[k], 'current': new[k], 'key': k, } for k in changed), ] # exclude fields not present in _meta.get_fields fields = {f.name: f for f in instance._meta.get_fields()} extra = { f.attname: f for f in instance._meta.get_fields() if hasattr(f, 'attname') } fields = {**extra, **fields} summary = [s for s in summary if s['key'] in fields.keys()] # field exclusion summary = [ s for s in summary if not field_exclusion(s['key'], instance, instance.__class__) ] model = ModelMirror() model.name = sender.__name__ model.application = Application(name=instance._meta.app_label) modifications = [] for entry in summary: field = ModelField() field.name = entry['key'] field.mirror = model field.type = fields[entry['key']].__class__.__name__ modification = ModelValueModification() modification.operation = entry['operation'] modification.field = field modification.previous = normalize_save_value(entry['previous']) modification.current = normalize_save_value(entry['current']) modifications.append(modification) instance._meta.dal.modifications = modifications if settings.model.performance: instance._meta.dal.performance = datetime.now()