Skip to content

Instantly share code, notes, and snippets.

@stephen2m
Forked from ngse/audit_mixin.py
Created March 14, 2020 07:42
Show Gist options
  • Save stephen2m/b234c1b854b74c19508104f45c96ca9d to your computer and use it in GitHub Desktop.
Save stephen2m/b234c1b854b74c19508104f45c96ca9d to your computer and use it in GitHub Desktop.
Rough attempt at implementing an audit log against Flask/Flask Login/SQLAlchemy models
# Requires use of Flask Login for the user tracking
# Implement by using AuditableMixin in your model class declarations
# e.g. class ImportantThing(AuditableMixin, Base):
import json
from flask_login import current_user
from sqlalchemy import event, inspect
from sqlalchemy.orm import class_mapper
from sqlalchemy.orm.attributes import get_history
from app import db
ACTION_CREATE = 1
ACTION_UPDATE = 2
ACTION_DELETE = 3
def _current_user_id_or_none():
try:
return current_user.id
except:
return None
class Base(db.Model):
"""Base model class to implement db columns and features every model should have"""
__abstract__ = True
def __tablename__(cls):
return cls.__name__.lower()
id = db.Column(db.Integer, primary_key=True, nullable=False)
class TimestampableMixin:
"""Allow a model to track its creation and update times"""
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
updated_at = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp())
class AuditLog(TimestampableMixin, Base):
"""Model an audit log of user actions"""
user_id = db.Column(db.Integer, doc="The ID of the user who made the change")
target_type = db.Column(db.String(100), nullable=False, doc="The table name of the altered object")
target_id = db.Column(db.Integer, doc="The ID of the altered object")
action = db.Column(db.Integer, doc="Create (1), update (2), or delete (3)")
state_before = db.Column(db.Text, doc="Stores a JSON string representation of a dict containing the altered column "
"names and original values")
state_after = db.Column(db.Text, doc="Stores a JSON string representation of a dict containing the altered column "
"names and new values")
def __init__(self, target_type, target_id, action, state_before, state_after):
self.user_id = _current_user_id_or_none()
self.target_type = target_type
self.target_id = target_id
self.action = action
self.state_before = state_before
self.state_after = state_after
def __repr__(self):
return '<AuditLog %r: %r -> %r>' % (self.user_id, self.target_type, self.action)
def save(self, connection):
connection.execute(
self.__table__.insert(),
user_id=self.user_id,
target_type=self.target_type,
target_id=self.target_id,
action=self.action,
state_before=self.state_before,
state_after=self.state_after
)
class AuditableMixin:
"""Allow a model to be automatically audited"""
@staticmethod
def create_audit(connection, object_type, object_id, action, **kwargs):
audit = AuditLog(
object_type,
object_id,
action,
kwargs.get('state_before'),
kwargs.get('state_after')
)
audit.save(connection)
@classmethod
def __declare_last__(cls):
event.listen(cls, 'after_insert', cls.audit_insert)
event.listen(cls, 'after_delete', cls.audit_delete)
event.listen(cls, 'after_update', cls.audit_update)
@staticmethod
def audit_insert(mapper, connection, target):
"""Listen for the `after_insert` event and create an AuditLog entry"""
target.create_audit(connection, target.__tablename__, target.id, ACTION_CREATE)
@staticmethod
def audit_delete(mapper, connection, target):
"""Listen for the `after_delete` event and create an AuditLog entry"""
target.create_audit(connection, target.__tablename__, target.id, ACTION_DELETE)
@staticmethod
def audit_update(mapper, connection, target):
"""Listen for the `after_update` event and create an AuditLog entry with before and after state changes"""
state_before = {}
state_after = {}
inspr = inspect(target)
attrs = class_mapper(target.__class__).column_attrs
for attr in attrs:
hist = getattr(inspr.attrs, attr.key).history
if hist.has_changes():
state_before[attr.key] = get_history(target, attr.key)[2].pop()
state_after[attr.key] = getattr(target, attr.key)
target.create_audit(connection, target.__tablename__, target.id, ACTION_UPDATE,
state_before=json.dumps(state_before),
state_after=json.dumps(state_after))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment