247 lines
8.1 KiB
Python
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()
|
|
|
|
|