diff --git a/src/main/settings_global.py b/src/main/settings_global.py
index 69ff0e9..6d08df6 100644
--- a/src/main/settings_global.py
+++ b/src/main/settings_global.py
@@ -52,6 +52,7 @@ INSTALLED_APPS = [
'anymail',
+ 'records_audit',
'nalodeni',
]
diff --git a/src/nalodeni/forms.py b/src/nalodeni/forms.py
index 9c7858a..1bd5dd2 100644
--- a/src/nalodeni/forms.py
+++ b/src/nalodeni/forms.py
@@ -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:
diff --git a/src/nalodeni/models.py b/src/nalodeni/models.py
index cbf03fd..eb42409 100644
--- a/src/nalodeni/models.py
+++ b/src/nalodeni/models.py
@@ -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.
"""
diff --git a/src/nalodeni/people.py b/src/nalodeni/people.py
index 0acf1d0..66dd495 100644
--- a/src/nalodeni/people.py
+++ b/src/nalodeni/people.py
@@ -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)
+
+
diff --git a/src/nalodeni/static/img/flags/cs.png b/src/nalodeni/static/img/flags/cs.png
new file mode 100644
index 0000000..872e750
Binary files /dev/null and b/src/nalodeni/static/img/flags/cs.png differ
diff --git a/src/nalodeni/static/img/flags/en.png b/src/nalodeni/static/img/flags/en.png
new file mode 100644
index 0000000..2c55443
Binary files /dev/null and b/src/nalodeni/static/img/flags/en.png differ
diff --git a/src/nalodeni/static/img/flags/sk.png b/src/nalodeni/static/img/flags/sk.png
new file mode 100644
index 0000000..87cc698
Binary files /dev/null and b/src/nalodeni/static/img/flags/sk.png differ
diff --git a/src/nalodeni/templates/people/list.html b/src/nalodeni/templates/people/list.html
index f764b16..9a2b7f4 100644
--- a/src/nalodeni/templates/people/list.html
+++ b/src/nalodeni/templates/people/list.html
@@ -61,7 +61,10 @@ $(document).ready(function(){
{{p.get_district_display}} |
{{p.get_kind_display}} |
{{p.interestedIn|default_if_none:'-'}} |
- |
+
+ detail,
+ upravit
+ |
{% endfor %}
diff --git a/src/nalodeni/templates/person/detail.html b/src/nalodeni/templates/person/detail.html
new file mode 100644
index 0000000..75c136e
--- /dev/null
+++ b/src/nalodeni/templates/person/detail.html
@@ -0,0 +1,59 @@
+{% extends 'pirati_cz.html' %}
+
+{%block head%}
+
+
+{%endblock%}
+
+{%block body%}
+
+
+
+ Detail uživatele (zpět,
+ upravit)
+
+ Uživ. jméno (login) | {{obj.username}} |
+ Jméno a příjmení | {{obj.first_name|default_if_none:'-'}} {{obj.last_name|default_if_none:'-'}} |
+ Město, PSČ, Kraj | {{obj.city|default_if_none:'-'}}, {{obj.postcode|default_if_none:'-'}}, {{obj.get_district_display|default_if_none:'-'}} |
+ |
+ Reg. e-mail | {{obj.email}} |
+ Kontaktní e-mail | {{obj.email_contact|default_if_none:'-'}}{% if not obj.email_contact_verified %} (neověřen){%endif%} |
+ |
+ Stav | {{obj.get_status_display}} |
+ Chci | {{obj.get_kind_display}} |
+ |
+ Zájmy |
+ {% for i in obj.userform.topics.all %}{{i.name}} {%endfor%}
+ |
+ Dovednosti |
+ {% for i in obj.userform.skills.all %}{{i.name}} {%endfor%}
+ |
+ Regiony |
+ {% for i in obj.userform.regions.all %}{{i.name}} {%endfor%}
+ |
+ |
+ Datum registrace | {{obj.createdStamp}} |
+ Datum souhlasu os. údajů | {{obj.dc_stamp|default_if_none:'-'}} |
+
+
+
+
+
+
+
+
+{%endblock%}
diff --git a/src/nalodeni/templates/person/edit.html b/src/nalodeni/templates/person/edit.html
new file mode 100644
index 0000000..555ac0c
--- /dev/null
+++ b/src/nalodeni/templates/person/edit.html
@@ -0,0 +1,22 @@
+{% extends 'pirati_cz.html' %}
+
+{%block body%}
+
+
+
+
+
+
Profil uživatele {{boj}}
+
+
+
+
+
+
+
+{%endblock%}
+
diff --git a/src/nalodeni/templates/pirati_cz.html b/src/nalodeni/templates/pirati_cz.html
index cbd28a8..c12ebc2 100644
--- a/src/nalodeni/templates/pirati_cz.html
+++ b/src/nalodeni/templates/pirati_cz.html
@@ -349,11 +349,6 @@
-
-
-
-
-
diff --git a/src/nalodeni/templates/pirati_cz_euro2019.html b/src/nalodeni/templates/pirati_cz_euro2019.html
index 37964e2..3441252 100644
--- a/src/nalodeni/templates/pirati_cz_euro2019.html
+++ b/src/nalodeni/templates/pirati_cz_euro2019.html
@@ -10,7 +10,7 @@
-
+
@@ -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; }
+
{% block headstyle %}
{% endblock %}
@@ -189,14 +191,15 @@
diff --git a/src/nalodeni/urls.py b/src/nalodeni/urls.py
index 71490e6..2de5261 100644
--- a/src/nalodeni/urls.py
+++ b/src/nalodeni/urls.py
@@ -29,6 +29,9 @@ urlpatterns = [
url(r'^ja-pirat/email-vizitka/$', views.email_vizitka, name="email_vizitka"),
+ url(r'^person/(?P[0-9]+)/$', people.person_detail, name="person_detail"),
+ url(r'^person/(?P[0-9]+)/edit$', people.person_edit, name="person_edit"),
+
url(r'^people/list/$', people.confirmed, name="people_list"),
url(r'^people/list/(?P[0-9]+)/$', people.confirmed, name="people_list"),
url(r'^people/list-new/$', people.confirmed, name="people_list_new", kwargs={'newOnly':True}),
diff --git a/src/nalodeni/views.py b/src/nalodeni/views.py
index 2913a75..a408d54 100644
--- a/src/nalodeni/views.py
+++ b/src/nalodeni/views.py
@@ -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
diff --git a/src/records_audit/__init__.py b/src/records_audit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/records_audit/migrations/0001_initial.py b/src/records_audit/migrations/0001_initial.py
new file mode 100644
index 0000000..b240c79
--- /dev/null
+++ b/src/records_audit/migrations/0001_initial.py
@@ -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',)},
+ ),
+ ]
diff --git a/src/records_audit/migrations/0002_auto_20190411_2008.py b/src/records_audit/migrations/0002_auto_20190411_2008.py
new file mode 100644
index 0000000..a6071ec
--- /dev/null
+++ b/src/records_audit/migrations/0002_auto_20190411_2008.py
@@ -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')},
+ ),
+ ]
diff --git a/src/records_audit/migrations/0003_auto_20190411_2010.py b/src/records_audit/migrations/0003_auto_20190411_2010.py
new file mode 100644
index 0000000..8b85763
--- /dev/null
+++ b/src/records_audit/migrations/0003_auto_20190411_2010.py
@@ -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',
+ ),
+ ]
diff --git a/src/records_audit/migrations/__init__.py b/src/records_audit/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/records_audit/models.py b/src/records_audit/models.py
new file mode 100644
index 0000000..1c4e60b
--- /dev/null
+++ b/src/records_audit/models.py
@@ -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.
+ """
+
+
diff --git a/src/records_audit/utils.py b/src/records_audit/utils.py
new file mode 100644
index 0000000..635ea58
--- /dev/null
+++ b/src/records_audit/utils.py
@@ -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()
+
+