Records_audit, EUweb, editace uzivatele

This commit is contained in:
Martin Rejman 2019-04-11 23:57:56 +02:00
parent c1093a54fe
commit 5d8f40390c
21 changed files with 638 additions and 43 deletions

View File

@ -52,6 +52,7 @@ INSTALLED_APPS = [
'anymail',
'records_audit',
'nalodeni',
]

View File

@ -17,7 +17,8 @@ class AppUserForm(ModelForm):
class Meta:
model = models.AppUser
fields = ['first_name','last_name', 'city', 'postcode', 'district', 'kind',
fields = ['first_name','last_name',
'city', 'postcode', 'district', 'kind',
'email', 'email_contact', 'email_contact_active', 'dc_stamp']
def clean_postcode(self):
@ -61,6 +62,35 @@ class AppUserSsoForm(ModelForm):
fields = ['city', 'postcode', 'district', 'kind',
'email', 'email_contact', 'email_contact_active', 'dc_stamp']
def clean_postcode(self):
data = self.cleaned_data['postcode']
if data is not None:
if data < 10000 or data > 99999:
raise ValidationError(_('PSČ musí být číslo mezi 10000 a 99999.'), code='invalid')
return data
def clean_first_name(self):
data = self.cleaned_data['first_name']
if data is not None:
if len(data) > 30:
raise ValidationError(_('Jméno může mít maximálně 30 znaků.'), code='invalid')
return data
def clean_last_name(self):
data = self.cleaned_data['last_name']
if data is not None:
if len(data) > 30:
raise ValidationError(_('Příjmení může mít maximálně 30 znaků.'), code='invalid')
return data
def clean_city(self):
data = self.cleaned_data['city']
if data is not None:
if len(data) > 50:
raise ValidationError(_('Město může mít maximálně 50 znaků.'), code='invalid')
return data
class AppRegEmailForm(ModelForm):
class Meta:

View File

@ -16,8 +16,10 @@ from django.conf import settings as appSettings
from keycloak_oidc import exceptions as sso_exc
from records_audit.utils import DataAudited
class AppUser(AbstractUser):
class AppUser(AbstractUser, DataAudited):
"""
Prepare an empty User class just in case it will be needed later.
"""
@ -241,12 +243,15 @@ class AppUser(AbstractUser):
else:
return self.email
_audit_fields = ('postcode', 'district', 'kind', 'password')
_audit_fields_exclude = ('emailToken',)
_audit_fields_private = ('password',)
class Meta:
verbose_name = _('AppUser')
verbose_name_plural = _('AppUsers')
unique_together = (("email", ),("username",), ("emailToken",),)
ordering = ('username',)
##
# Permission functions
@ -355,7 +360,7 @@ class UserTopic(Model):
class UserForm(Model):
class UserForm(Model, DataAudited):
"""
Dotaznik ohledne dovednosti a schopnosti uzivatele.
"""

View File

@ -6,7 +6,7 @@ from collections import OrderedDict
import django
from django.http import HttpResponse, HttpResponseRedirect
from django.template import Template, RequestContext, loader
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.urls import reverse
@ -195,3 +195,74 @@ def update(request):
return HttpResponseRedirect('/people/list/')
@login_required(login_url="/prihlaseni")
@transaction.atomic
@role_required(['sso_kodo'])
def person_detail(request, id):
obj = models.USER_MODEL.objects.get(pk=id)
sp = request.session['site_perms']
sp_sso_kodo = 'sso_kodo' in sp
sp_sso_admin = 'sso_admin' in sp
# Check permissions to edit this object
if not sp_sso_admin and not obj.district in get_AppUser_districts(request):
messages.error(request, 'K tomuto záznamu nemáte přístup. ')
return redirect('nalodeni:people_list')
template = 'person/detail.html'
context = {
'obj' : obj,
}
return render(request, template, context)
@login_required(login_url="/prihlaseni")
@transaction.atomic
@role_required(['sso_kodo'])
def person_edit(request, id):
obj = models.USER_MODEL.objects.get(pk=id)
sp = request.session['site_perms']
sp_sso_kodo = 'sso_kodo' in sp
sp_sso_admin = 'sso_admin' in sp
# Check permissions to edit this object
if not sp_sso_admin and not obj.district in get_AppUser_districts(request):
messages.error(request, 'K tomuto záznamu nemáte přístup. ')
return redirect('nalodeni:people_list')
if obj.ssoUid:
_form = forms.AppUserSsoForm
else:
_form = forms.AppUserForm
if request.method == "GET":
form = _form(instance=obj)
elif request.method == "POST":
with obj.audit_context(request.user) as ac:
form = _form(request.POST, instance=obj)
if form.is_valid():
form.save()
messages.info(request, "Údaje byly uloženy.")
return redirect('nalodeni:person_detail', form.instance.id)
else:
messages.error(request, "Opravte prosím chyby v zadání.")
raise ac.DataNotSavedException
else:
form = None
template = 'person/edit.html'
context = {
'obj' : obj,
'form' : form,
'AUTH_SERVER' : appSettings.AUTH_SERVER,
}
return render(request, template, context)

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -61,7 +61,10 @@ $(document).ready(function(){
<td>{{p.get_district_display}}</td>
<td>{{p.get_kind_display}}</td>
<td>{{p.interestedIn|default_if_none:'-'}}</td>
<td></td>
<td>
<a href="{% url 'nalodeni:person_detail' p.id%}">detail</a>,
<a href="{% url 'nalodeni:person_edit' p.id%}">upravit</a>
</td>
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,59 @@
{% extends 'pirati_cz.html' %}
{%block head%}
<script>
</script>
<style>
.red { color: #f55; }
table th, table td { vertical-align: top; }
table td { text-align: left; }
table th { text-align: right; }
</style>
{%endblock%}
{%block body%}
<div class="row">
<div class="medium-12 large-12 columns">
<section class="o-section o-section--spaceBot">
<h2>Detail uživatele <small>(<a href="{% url 'nalodeni:people_list'%}">zpět</a>,
<a href="{% url 'nalodeni:person_edit' obj.id %}">upravit</a>)</small></h2>
<table style="width: auto;">
<tr><th>Uživ. jméno (login)</th><td>{{obj.username}}</td></tr>
<tr><th>Jméno a příjmení</th><td>{{obj.first_name|default_if_none:'-'}} {{obj.last_name|default_if_none:'-'}}</td></tr>
<tr><th>Město, PSČ, Kraj</th><td>{{obj.city|default_if_none:'-'}}, {{obj.postcode|default_if_none:'-'}}, {{obj.get_district_display|default_if_none:'-'}}</td></tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr><th>Reg. e-mail</th><td>{{obj.email}}</td></tr>
<tr><th>Kontaktní e-mail</th><td{% if not obj.email_contact_verified %} class="red"{%endif%}>{{obj.email_contact|default_if_none:'-'}}{% if not obj.email_contact_verified %} (neověřen){%endif%}</td></tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr><th>Stav</th><td>{{obj.get_status_display}}</td></tr>
<tr><th>Chci</th><td>{{obj.get_kind_display}}</td></tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr><th>Zájmy</th><td>
{% for i in obj.userform.topics.all %}{{i.name}}<br/>{%endfor%}
</td></tr>
<tr><th>Dovednosti</th><td>
{% for i in obj.userform.skills.all %}{{i.name}}<br/>{%endfor%}
</td></tr>
<tr><th>Regiony</th><td>
{% for i in obj.userform.regions.all %}{{i.name}}<br/>{%endfor%}
</td></tr>
<tr><td colspan="2">&nbsp;</td></tr>
<tr><th>Datum registrace</th><td>{{obj.createdStamp}}</td></tr>
<tr><th>Datum souhlasu os. údajů</th><td>{{obj.dc_stamp|default_if_none:'-'}}</td></tr>
</table>
</section>
</div>
</div>
<div class="row">
<div class="medium-12 large-12 columns">
<section class="o-section o-section--spaceBot">
<div class="o-section-inner">
&nbsp;
</div>
</section>
</div>
</div>
{%endblock%}

View File

@ -0,0 +1,22 @@
{% extends 'pirati_cz.html' %}
{%block body%}
<div class="row">
<div class="medium-12 large-12 columns">
<section class="o-section o-section--spaceBot">
<div class="o-section-inner">
<div class="o-section-block" data-equalizer data-equalize-on="medium">
<h2>Profil uživatele {{boj}}</h2>
<form method="post" action="#">
{%csrf_token%}
{{form}}
<input type="submit" class="button button-primary" value="Uložit provedené změny"/>
</form>
</div>
</div>
</section>
</div>
</div>
{%endblock%}

View File

@ -349,11 +349,6 @@
<i class="fa fa-instagram c-icon c-icon--rounded c-icon--instagram" aria-hidden="true"></i>
</a>
</li>
<li class="c-vertical-navigation__item">
<a href="https://plus.google.com/+piratskastrana" rel="noopener noreferrer" title="Google Plus - Česká pirátská strana">
<i class="fa fa-google-plus c-icon c-icon--rounded c-icon--googlePlus" aria-hidden="true"></i>
</a>
</li>
</ul>
</nav>
<p class="u-10paddingTop u-maxWidth80percent u-smaller-line-height hide-for-large">

View File

@ -10,7 +10,7 @@
<meta name="description" content="Česká pirátská strana kandiduje v eurovolbách 2019 do evropského parlamentu.">
<meta name="keywords" content="piráti, česká pirátská strana, " />
<meta property="og:title" content="Evropa potřebuje Piráty!" />
<meta property="og:description" content="Podpořte Piráty v kandidatuře do evrpského parlamentu." />
<meta property="og:description" content="Podpořte Piráty v kandidatuře do evropského parlamentu." />
<meta property="og:image" content="https://evropapotrebuje.cz/static/img/celo-kandidatky-pirati-2019.png" />
<meta property="og:type" content="website" />
@ -167,6 +167,8 @@
.o-section { padding-top: 35px; }
.flag {border: 1px solid black; height: 0.5em; width: 0.8em; margin: 0.25em; vertical-align: baseline; }
</style>
{% block headstyle %}
{% endblock %}
@ -189,14 +191,15 @@
</div>
<div class="header-right">
<div><a href="https://evropapotrebuje.cz/program/">Volební program</a></div>
<!--
<div><a href="https://evropapotrebuje.cz/jak-volit/">Jak volit</a></div>
-->
<div><a href="https://evropapotrebuje.cz/kandidati/">Kandidáti</a></div>
<div><a href="https://evropapotrebuje.cz/zapoj-se/">Zapoj se</a></div>
<!--
<div><a href="https://evropapotrebuje.cz/proc-jit-volit/">Proč jít volit</a></div>
-->
<div>
<a href="https://evropapotrebuje.cz/en/how-to-vote/"><img class="flag" src="/static/img/flags/en.png"></a>
</div>
</div>
</div>
</div>

View File

@ -29,6 +29,9 @@ urlpatterns = [
url(r'^ja-pirat/email-vizitka/$', views.email_vizitka, name="email_vizitka"),
url(r'^person/(?P<id>[0-9]+)/$', people.person_detail, name="person_detail"),
url(r'^person/(?P<id>[0-9]+)/edit$', people.person_edit, name="person_edit"),
url(r'^people/list/$', people.confirmed, name="people_list"),
url(r'^people/list/(?P<dist>[0-9]+)/$', people.confirmed, name="people_list"),
url(r'^people/list-new/$', people.confirmed, name="people_list_new", kwargs={'newOnly':True}),

View File

@ -555,34 +555,36 @@ def profil(request):
elif request.method == "POST":
form = _form(request.POST, instance=request.user)
email_contact_orig = request.user.email_contact
if form.is_valid():
email_contact_changed = email_contact_orig != form.instance.email_contact
with request.user.audit_context(request.user) as audit:
if form.is_valid():
email_contact_changed = email_contact_orig != form.instance.email_contact
if email_contact_changed:
form.instance.email_contact_verified = False
form.save()
if email_contact_changed:
form.instance.email_contact_verified = False
form.save()
if (form.instance.email_contact != None
and not form.instance.email_contact_verified):
try:
nalodeni_auth.sendEmailContactVerificationToken(form.instance)
if (form.instance.email_contact != None
and not form.instance.email_contact_verified):
try:
nalodeni_auth.sendEmailContactVerificationToken(form.instance)
messages.info(request,
"Potvrzovací email pro nastavení kontaktní adresy byl odeslán.")
except ValidationError as e:
messages.error(request, "Kontaktní email není správně vyplněn.")
except Exception as e:
logger.error("Chyba pri odesilani email_contact potvrzeni: %s" % e)
messages.error(request,
"Potvrzovací email pro změnu kontaktní adresy " +
"se nepodařilo odeslat.")
messages.info(request,
"Potvrzovací email pro nastavení kontaktní adresy byl odeslán.")
except ValidationError as e:
messages.error(request, "Kontaktní email není správně vyplněn.")
except Exception as e:
logger.error("Chyba pri odesilani email_contact potvrzeni: %s" % e)
messages.error(request,
"Potvrzovací email pro změnu kontaktní adresy " +
"se nepodařilo odeslat.")
messages.info(request, "Údaje byly uloženy.")
return redirect('nalodeni:ja_pirat')
messages.info(request, "Údaje byly uloženy.")
return redirect('nalodeni:ja_pirat')
else:
messages.error(request, "Opravte prosím chyby v zadání.")
else:
messages.error(request, "Opravte prosím chyby v zadání.")
raise audit.DataNotSavedException
else:
form = None
@ -614,15 +616,17 @@ def dotaznik(request):
form = _form(instance=request.user.userform)
elif request.method == "POST":
form = _form(request.POST, instance=request.user.userform)
if form.is_valid():
form.save()
with request.user.userform.audit_context(request.user) as audit:
form = _form(request.POST, instance=request.user.userform)
if form.is_valid():
form.save()
messages.info(request, "Údaje byly uloženy.")
return redirect('nalodeni:ja_pirat')
messages.info(request, "Údaje byly uloženy.")
return redirect('nalodeni:ja_pirat')
else:
messages.error(request, "Opravte prosím chyby v zadání.")
else:
messages.error(request, "Opravte prosím chyby v zadání.")
raise audit.DataNotSavedException
else:
form = None

View File

View File

@ -0,0 +1,62 @@
# Generated by Django 2.0.3 on 2019-04-11 18:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AuditField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=150, verbose_name='Field name')),
],
),
migrations.CreateModel(
name='AuditLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ts', models.DateTimeField(editable=False, verbose_name='Timestamp')),
('tnum', models.IntegerField(default='-1', editable=False, verbose_name='Transaction number')),
('user', models.IntegerField(editable=False, null=True, verbose_name='User ID')),
('rid', models.IntegerField(editable=False, verbose_name='Table record ID')),
('val', models.CharField(max_length=500, verbose_name='Field value')),
('fld', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to='records_audit.AuditField', verbose_name='Audited field')),
],
options={
'ordering': ('ts', 'tnum'),
},
),
migrations.CreateModel(
name='AuditTable',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=150, verbose_name='Table name')),
],
),
migrations.AlterUniqueTogether(
name='audittable',
unique_together={('name',)},
),
migrations.AddField(
model_name='auditlog',
name='table',
field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to='records_audit.AuditTable', verbose_name='Audited table'),
),
migrations.AddField(
model_name='auditfield',
name='table',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='records_audit.AuditTable', verbose_name='Audited table'),
),
migrations.AlterUniqueTogether(
name='auditfield',
unique_together={('name',)},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.0.3 on 2019-04-11 20:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('records_audit', '0001_initial'),
]
operations = [
migrations.AlterUniqueTogether(
name='auditfield',
unique_together={('table', 'name')},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.3 on 2019-04-11 20:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('records_audit', '0002_auto_20190411_2008'),
]
operations = [
migrations.RenameField(
model_name='auditlog',
old_name='user',
new_name='user_id',
),
]

View File

View File

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
import datetime
import django
from django.db import models
from django.utils.translation import ugettext_lazy as _
class AuditTable(models.Model):
""" Translation table for names of audited tables
"""
name = models.CharField(_('Table name'), max_length=150)
class Meta:
unique_together = (('name',),)
class AuditField(models.Model):
""" Audited fields in the table.
"""
table = models.ForeignKey(AuditTable, verbose_name=_('Audited table'), on_delete=models.PROTECT)
name = models.CharField(_('Field name'), max_length=150)
class Meta:
unique_together = (('table','name',),)
class AuditLog(models.Model):
""" Track changes in records. """
ts = models.DateTimeField(_('Timestamp'),
null=False, editable=False)
tnum = models.IntegerField(_('Transaction number'), default = "-1",
null=False, editable=False)
""" This number is unique within one AuditContext run. """
user_id = models.IntegerField(verbose_name=_('User ID'),
null=True, editable=False)
""" The user who performed this action. None, if unknown.
The ID stored is the user-id from the get_user_model() table. Due to
circular reference, when auditing get_user_model() table, this is just
plain integer, not ForeignKey.
"""
table = models.ForeignKey(AuditTable, verbose_name=_('Audited table'), on_delete=models.PROTECT,
null=False, editable=False)
fld = models.ForeignKey(AuditField, verbose_name=_('Audited field'), on_delete=models.PROTECT,
null=False, editable=False)
rid = models.IntegerField(_('Table record ID'),
null=False, editable=False)
val = models.CharField(_('Field value'), max_length=500)
""" The latest value is stored in the actual model table. This value
is the previous one.
The store timestamp is the last Datetime when the value Val was valid.
"""
class Meta:
ordering = ('ts', 'tnum',)
""" We order by transaction start timestamp, then by 'tnum', because
a nested transaction can be saved earlier than the parent object.
"""

233
src/records_audit/utils.py Normal file
View File

@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
import datetime
import django
from django.db import models as dj_models
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__
#
# check and update the object cache
#
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
# 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:
# 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
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()