mongoengine icon indicating copy to clipboard operation
mongoengine copied to clipboard

Post save signal with the delta information

Open rminsk opened this issue 8 years ago • 3 comments

I am looking at logging changes to some of my documents for auditing. Based on Issue #589 the post_save signal is now being sent before the changed fields are being cleared. It would be nice if somehow the updates/removals (self._delta) could be passed to the signal. Right now I have to recompute the delta. I'm not quite sure how this could be done in a backwards compatible way unless everyone receiver is using **kwargs. Maybe a new signal could be added.

rminsk avatar Jun 18 '16 03:06 rminsk

This is an example of what I would like to do. Would like to avoid having to call document._delta() because it has already been computed and it is a "protected" member of the Document class.

import json
from datetime import datetime
from bson import json_util
import mongoengine as me


class Change(me.EmbeddedDocument):
    """The change to the mongo record"""
    op = me.StringField(required=True, choices=['insert', 'set', 'unset', 'delete'])  # pylint: disable=invalid-name
    data = me.DictField()


class AuditLog(me.Document):
    """Track changes to other documents"""
    object = me.GenericReferenceField()
    date = me.DateTimeField(required=True, default=datetime.utcnow)
    user = me.StringField()
    changes = me.EmbeddedDocumentListField(Change)


def audit(cls):
    """Class decorator to audit a Document subclass"""
    me.signals.post_save.connect(_post_save_audit, sender=cls)
    me.signals.post_delete.connect(_post_delete_audit, sender=cls)
    return cls


def _post_save_audit(sender, document, created, **kwargs):  # pylint: disable=unused-argument
    changes = []
    if created:
        changes.append(Change(op='insert', data=document.to_mongo()))
    else:
        updates, removals = document._delta()  # pylint: disable=protected-access
        if updates:
            changes.append(Change(op='set', data=updates))
        if removals:
            changes.append(Change(op='unset', data=removals))
    log_entry = AuditLog(object=document, changes=changes)
    log_entry.save()


def _post_delete_audit(sender, document, **kwargs):  # pylint: disable=unused-argument
    change = Change(op='delete', data=document.to_mongo())
    log_entry = AuditLog(object=document, changes=[change])
    log_entry.save()

rminsk avatar Jun 22 '16 22:06 rminsk

BTW, you can do this in the pre_save* signals by comparing the state of the not-yet-saved document's fields to its previous state stored in Mongo. As an example:

def pre_save_post_validation(sender, document, **kwargs):
    old_document = type(document).objects.get(id=str(document.id))
    for changed_field_name in document._get_changed_fields():
        value = getattr(document, changed_field_name)
        old_value = getattr(old_document, changed_field_name)
        # ...

esmail avatar Feb 09 '18 13:02 esmail

I know this is ancient history, but... Is there any improvement to this pattern? I went looking and couldn't find anything. I'm a little skeptical about depending on an internal method of the library to do this sort of comparison.

andrewsw avatar Aug 31 '22 19:08 andrewsw