Esempio n. 1
0
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)
Esempio n. 2
0
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)
Esempio n. 3
0
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)
Esempio n. 4
0
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()