Records_audit, EUweb, editace uzivatele
This commit is contained in:
parent
c1093a54fe
commit
5d8f40390c
|
@ -52,6 +52,7 @@ INSTALLED_APPS = [
|
|||
|
||||
'anymail',
|
||||
|
||||
'records_audit',
|
||||
'nalodeni',
|
||||
]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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"> </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"> </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"> </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"> </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">
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{%endblock%}
|
|
@ -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%}
|
||||
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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',)},
|
||||
),
|
||||
]
|
|
@ -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')},
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
Loading…
Reference in New Issue