nalodeni.pirati.cz/records_audit/utils.py

247 lines
8.1 KiB
Python

# -*- coding: utf-8 -*-
import datetime
import django
from django.db import models as dj_models
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from . import models
_table_cache = {}
#models.AuditTable.objects.all().delete()
def extract_object_data(obj):
"""
Extract data fields from a Model object.
"""
af_fields = [] # array of Field objects
af = getattr(obj, '_audit_fields', None)
afe = getattr(obj, '_audit_fields_exclude', None)
if af:
for f in af:
if afe and f in afe: # respect exclude fields
continue
af_fields.append(obj._meta.get_field(f))
else:
if afe:
for f in obj._meta.get_fields():
if afe and f.name in afe: # respect exclude fields
continue
af_fields.append(f)
else:
af_fields = obj._meta.get_fields()
# walk through data fields, extract values
data = {}
for df in obj._meta.get_fields():
# skip if audit_fields is explicitly defined
if af and df.name not in af:
continue
if df.concrete:
if type(df) == dj_models.ForeignKey:
data[df.name] = getattr(obj, df.name+"_id")
elif type(df) == dj_models.ManyToManyField:
data[df.name] = set(getattr(obj, df.name).all().values_list('pk', flat=True))
else:
data[df.name] = getattr(obj, df.name)
return data
class DataAudited:
"""
Ensure auditing of changes by storing updated fields in a changelog
with timestamp, change-transaction-id and the user who made the change.
"""
class DataNotSavedException(Exception):
"""
Throw this exception when data was partially saved,
but the transaction will be rolled back. Then, we should
not save the diff, because it will actually not happen.
"""
pass
def __init__(self):
self.__audit_table = None
""" Link to the object structure cache. """
self.__orig_data = None
""" Is None when not in DataAudited "with" block of code. """
self.__audit_user = None
""" The user actually doing the changes in DB. """
self.__save = self.save
""" Original save method. """
self.__audit_start_ts = None
""" Timestamp of AuditContext creation. """
# setup our save method
self.save = self.__audited_save__
def __cache_setup__(self):
""" Check and update the object cache.
This calculation CAN NOT be done in __init__ because of
possible circular dependencies in the models.
"""
tbl_name = self._meta.db_table
if tbl_name in _table_cache:
self.__audit_table = _table_cache[tbl_name]
else:
# create the record
_table_cache[tbl_name] = {
'db_table' : tbl_name,
'id' : None,
'fields' : {},
}
ltc = _table_cache[tbl_name] # local table cache variable
self.__audit_table = ltc
# load or create AuditTable
rslt = models.AuditTable.objects.filter(name=tbl_name)
if len(rslt) == 1:
tbl = rslt[0]
elif len(rslt) == 0:
#print("Creating AuditTable", tbl_name )
tbl = models.AuditTable()
tbl.name = tbl_name
tbl.save()
else:
1/0
ltc['id'] = tbl.id
# update AuditFields
known_af = tbl.auditfield_set.all()
wanted_af = set(extract_object_data(self).keys())
for fld_obj in known_af:
if fld_obj.name in wanted_af:
# create cache record
ltc['fields'][fld_obj.name] = {
'id' : fld_obj.id,
'name' : fld_obj.name,
}
# remove known fields
wanted_af.remove(fld_obj.name)
# create remaining wanted_af (there can be some new)
for fld_name in wanted_af:
#print("Creating AuditField", tbl_name, fld_name )
fld = models.AuditField(table=tbl, name=fld_name)
fld.save()
# create cache record
ltc['fields'][fld_name] = {
'id' : fld.id,
'name' : fld_name,
}
def __audited_save__(self, *args, **kwargs):
""" We audit all save()s, not just the explicit.
We save the original save() method and provide our own
variant to ensure auditing of save()-s even outside
of the 'with' blocks of code.
This leaves only direct database updates unaudited.
"""
if self.__orig_data:
# probably already in DataAudited with block, just save
self.__save(*args, **kwargs)
else:
if self.id is None:
# just save
self.__save(*args, **kwargs)
else:
# do the audit
obj_orig = type(self).objects.get(pk=self.id)
self.__audit_start_ts = datetime.datetime.now()
self.__orig_data = extract_object_data(obj_orig)
self.__save(*args, **kwargs)
self.__exit__(None, None, None)
self.__orig_data = None
self.__audit_start_ts = None
def audit_context(self, user=None):
""" Setup self as the context.
We also record the user doing the changes.
Transaction start timestamp is recorded to differentiate
nested DataAudited blocks.
The transaction number 'tnum' is growing, but later TNUM
can be saved earlier then the parent object.
Thus, we use timestamp to mitigate for this - correct
Audit log ordering is (ts, tnum).
"""
self.__audit_user = user
self.__audit_start_ts = datetime.datetime.now()
return self
def __enter__(self):
#print("__enter__")
self.__orig_data = extract_object_data(self)
#for k in self.__orig_data:
# print(' %s -> %s' % (k, self.__orig_data[k]) )
return self
def __exit__(self, exc_type, exc_value, traceback):
#print("__exit__", exc_type, exc_value, traceback)
if exc_type:
if exc_type == self.DataNotSavedException:
return True
else:
return False
data_diff = {}
data_new = extract_object_data(self)
af_private = getattr(self, '_audit_fields_private', None)
# We assume no new fields appear after __enter__ and before __exit__,
# so we do not look for keys in data_new that are not present in data_orig.
for f in self.__orig_data:
if self.__orig_data[f] != data_new[f]:
if af_private and f in af_private:
self.__orig_data[f] = "< ... hidden ... >"
data_new[f] = "< ... hidden ... >"
data_diff[f] = ( self.__orig_data[f], data_new[f] )
self.__save_diff(data_diff)
self.__orig_data = None
self.__audit_start_ts = None
return True
def __save_diff(self, data_diff):
""" Save changes recorded in data_diff to DB. """
tnum = 1
if self.__audit_table is None:
self.__cache_setup__()
print("AuditLog", self.__audit_table['db_table'], 'id', self.id)
for k in data_diff:
print(' %s: \t %s -> %s' % (k, data_diff[k][0], data_diff[k][1]) )
al = models.AuditLog(
ts = self.__audit_start_ts,
tnum = tnum,
user_id = self.__audit_user.id if self.__audit_user else None,
table_id = self.__audit_table['id'],
fld_id = self.__audit_table['fields'][k]['id'],
rid = self.id,
)
al.val = data_diff[k][0]
al.save()