Merge lp:~mhall119/summit/add-auditing into lp:summit

Proposed by Michael Hall
Status: Needs review
Proposed branch: lp:~mhall119/summit/add-auditing
Merge into: lp:summit
Diff against target: 417 lines (+321/-0)
9 files modified
summit/common/admin/__init__.py (+1/-0)
summit/common/admin/auditadmin.py (+12/-0)
summit/common/audit.py (+131/-0)
summit/common/migrations/0003_add_audit_model.py (+125/-0)
summit/common/models.py (+36/-0)
summit/schedule/models/agendamodel.py (+5/-0)
summit/schedule/models/meetingmodel.py (+5/-0)
summit/schedule/models/summitmodel.py (+5/-0)
summit/settings.py (+1/-0)
To merge this branch: bzr merge lp:~mhall119/summit/add-auditing
Reviewer Review Type Date Requested Status
Summit Hackers Pending
Review via email: mp+150249@code.launchpad.net

Description of the change

Adds change auditing to Summit, Meeting and Agenda

To post a comment you must log in.

Unmerged revisions

484. By Michael Hall

Add auditing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'summit/common/admin/__init__.py'
2--- summit/common/admin/__init__.py 2012-03-08 01:55:17 +0000
3+++ summit/common/admin/__init__.py 2013-02-24 22:47:21 +0000
4@@ -15,3 +15,4 @@
5 # along with this program. If not, see <http://www.gnu.org/licenses/>.
6
7 from menuadmin import *
8+from auditadmin import *
9
10=== added file 'summit/common/admin/auditadmin.py'
11--- summit/common/admin/auditadmin.py 1970-01-01 00:00:00 +0000
12+++ summit/common/admin/auditadmin.py 2013-02-24 22:47:21 +0000
13@@ -0,0 +1,12 @@
14+from django.contrib import admin
15+from common.models import AuditRecord, AuditChange
16+
17+class AuditChangeInline(admin.TabularInline):
18+ model = AuditChange
19+
20+class AuditAdmin(admin.ModelAdmin):
21+ inlines = [AuditChangeInline]
22+ list_display = ('audit_date', 'change_type', 'app_name', 'model_name', 'model_id', 'user')
23+ list_filter = ('change_type', 'app_name', 'model_name')
24+
25+admin.site.register(AuditRecord, AuditAdmin)
26
27=== added file 'summit/common/audit.py'
28--- summit/common/audit.py 1970-01-01 00:00:00 +0000
29+++ summit/common/audit.py 2013-02-24 22:47:21 +0000
30@@ -0,0 +1,131 @@
31+"""
32+Hook existing objects into auditing sytem through Django signals
33+
34+Use:
35+
36+from common.audit import auditSave, auditDelete
37+from django.db.models.signals import pre_save, pre_delete
38+
39+pre_save.connect(auditSave, sender=YourObject)
40+pre_delete.connect(auditDelete, sender=YourObject)
41+
42+"""
43+import django
44+from models import AuditRecord, AuditChange
45+
46+
47+try:
48+ from threading import local
49+except ImportError:
50+ from django.utils._threading_local import local
51+
52+_thread_locals = local()
53+
54+
55+def get_current_user():
56+ "Return the owner of the current thread"
57+ return getattr(_thread_locals, 'user', None)
58+
59+def get_current_user_id():
60+ user = get_current_user()
61+ if (user != None):
62+ return getattr(user, 'id', 0)
63+ else:
64+ return 0
65+
66+def set_current_user(user):
67+ "Set the owner of the current thread"
68+ _thread_locals.user = user
69+
70+
71+class CaptureRequestUser(object):
72+ "Middleware that captures the current HTTP request user for auditing purposes"
73+
74+ def process_request(self, request):
75+ set_current_user(getattr(request, 'user', None))
76+
77+
78+def auditSave(sender, **kwargs):
79+ instance = kwargs['instance']
80+ if (instance.pk is not None and instance.pk > 0):
81+ try:
82+ old = instance.__class__.objects.get(id=instance.pk)
83+ except:
84+ old = instance.__class__()
85+ else:
86+ old = instance.__class__()
87+
88+ rec = AuditRecord()
89+ rec.user = get_current_user()
90+ rec.app_name = instance._meta.app_label;
91+ rec.model_name = instance.__class__.__name__
92+ rec.model_id = instance.id or 0
93+ if instance.id:
94+ rec.change_type = rec.AUDIT_MODIFY
95+ else:
96+ rec.change_type = rec.AUDIT_CREATE
97+
98+ rec.save()
99+
100+ fields_changed = rec.change_type == rec.AUDIT_CREATE
101+
102+ for f in get_audit_fields(instance):
103+ try:
104+ oldval = getattr(old, f, None)
105+ except:
106+ oldval = None
107+ newval = getattr(instance, f)
108+
109+ # For some reason oldval will be an int, when the field is infact a
110+ # boolean, so this will force oldval to be the right type
111+ if isinstance(newval, bool) and isinstance(oldval, int):
112+ oldval = bool(oldval)
113+
114+ if (isinstance(oldval, django.db.models.Model)):
115+ oldval = getattr(oldval, 'pk', oldval)
116+ if (isinstance(newval, django.db.models.Model)):
117+ newval = getattr(newval, 'pk', newval)
118+
119+ if ((oldval and oldval.__str__().strip() != '') or (newval and newval.__str__().strip() != '')) and not (oldval and newval and oldval.__str__().strip() == newval.__str__().strip()):
120+ if hasattr(instance, 'audit_censor') and f in instance.audit_censor:
121+ oldval = '*****'
122+ newval = '*****'
123+ change = AuditChange()
124+ change.record = rec
125+ change.field_name = f[0:50]
126+ change.old_val = str(oldval)[0:255]
127+ change.new_val = str(newval)[0:255]
128+ change.save()
129+ fields_changed = True
130+ if not fields_changed:
131+ rec.delete()
132+
133+
134+def auditDelete(sender, **kwargs):
135+
136+ instance = kwargs['instance']
137+
138+ rec = AuditRecord()
139+ rec.user = get_current_user()
140+ rec.app_name = instance._meta.app_label;
141+ rec.model_name = instance.__class__.__name__
142+ rec.model_id = instance.id or 0
143+ rec.change_type = rec.AUDIT_DELETE
144+ rec.save()
145+
146+def recordChange(rec, fieldname, oldval, newval):
147+ change = AuditChange()
148+ change.record = rec
149+ change.field_name = fieldname[0:50]
150+ change.old_val = str(oldval)[0:255]
151+ change.new_val = str(newval)[0:255]
152+ change.save()
153+
154+def get_audit_fields(instance):
155+ "Get a list of fields from a model for which value changes should be audited"
156+ # The use of _meta is not encouraged, as it is not an external API for Django
157+ # But I don't see any other reliable way to get a list of a model's fields.
158+ if hasattr(instance, 'audit_ignore'):
159+ return [f.column for f in instance._meta.fields if f.name not in instance.audit_ignore]
160+ else:
161+ return [f.column for f in instance._meta.fields]
162
163=== added file 'summit/common/migrations/0003_add_audit_model.py'
164--- summit/common/migrations/0003_add_audit_model.py 1970-01-01 00:00:00 +0000
165+++ summit/common/migrations/0003_add_audit_model.py 2013-02-24 22:47:21 +0000
166@@ -0,0 +1,125 @@
167+# encoding: utf-8
168+import datetime
169+from south.db import db
170+from south.v2 import SchemaMigration
171+from django.db import models
172+
173+class Migration(SchemaMigration):
174+
175+ def forwards(self, orm):
176+
177+ # Adding model 'AuditChange'
178+ db.create_table('common_auditchange', (
179+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
180+ ('record', self.gf('django.db.models.fields.related.ForeignKey')(related_name='changes', to=orm['common.AuditRecord'])),
181+ ('field_name', self.gf('django.db.models.fields.CharField')(max_length=50)),
182+ ('old_val', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
183+ ('new_val', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)),
184+ ))
185+ db.send_create_signal('common', ['AuditChange'])
186+
187+ # Adding model 'AuditRecord'
188+ db.create_table('common_auditrecord', (
189+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
190+ ('audit_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
191+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)),
192+ ('app_name', self.gf('django.db.models.fields.CharField')(max_length=50)),
193+ ('model_name', self.gf('django.db.models.fields.CharField')(max_length=50)),
194+ ('model_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
195+ ('change_type', self.gf('django.db.models.fields.PositiveIntegerField')()),
196+ ))
197+ db.send_create_signal('common', ['AuditRecord'])
198+
199+
200+ def backwards(self, orm):
201+
202+ # Deleting model 'AuditChange'
203+ db.delete_table('common_auditchange')
204+
205+ # Deleting model 'AuditRecord'
206+ db.delete_table('common_auditrecord')
207+
208+
209+ models = {
210+ 'auth.group': {
211+ 'Meta': {'object_name': 'Group'},
212+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
213+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
214+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
215+ },
216+ 'auth.permission': {
217+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
218+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
219+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
220+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
221+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
222+ },
223+ 'auth.user': {
224+ 'Meta': {'ordering': "['username']", 'object_name': 'User'},
225+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
226+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
227+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
228+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
229+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
230+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
231+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
232+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
233+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
234+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
235+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
236+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
237+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
238+ },
239+ 'common.auditchange': {
240+ 'Meta': {'object_name': 'AuditChange'},
241+ 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
242+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243+ 'new_val': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
244+ 'old_val': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
245+ 'record': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'changes'", 'to': "orm['common.AuditRecord']"})
246+ },
247+ 'common.auditrecord': {
248+ 'Meta': {'ordering': "['-audit_date']", 'object_name': 'AuditRecord'},
249+ 'app_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
250+ 'audit_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
251+ 'change_type': ('django.db.models.fields.PositiveIntegerField', [], {}),
252+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
253+ 'model_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
254+ 'model_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
255+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
256+ },
257+ 'common.menu': {
258+ 'Meta': {'object_name': 'Menu'},
259+ 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
260+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
261+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
262+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
263+ 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}),
264+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
265+ },
266+ 'common.menuitem': {
267+ 'Meta': {'object_name': 'MenuItem'},
268+ 'anonymous_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
269+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
270+ 'link_url': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
271+ 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
272+ 'menu': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['common.Menu']"}),
273+ 'order': ('django.db.models.fields.IntegerField', [], {}),
274+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
275+ },
276+ 'contenttypes.contenttype': {
277+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
278+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
279+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
280+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
281+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
282+ },
283+ 'sites.site': {
284+ 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
285+ 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
286+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
287+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
288+ }
289+ }
290+
291+ complete_apps = ['common']
292
293=== modified file 'summit/common/models.py'
294--- summit/common/models.py 2012-10-02 18:07:24 +0000
295+++ summit/common/models.py 2013-02-24 22:47:21 +0000
296@@ -2,6 +2,7 @@
297
298 from django.contrib.sites.models import Site
299 from django.contrib.sites.managers import CurrentSiteManager
300+from django.contrib.auth.models import User
301
302 class Menu(models.Model):
303 name = models.CharField(max_length=100)
304@@ -42,3 +43,38 @@
305
306 def __unicode__(self):
307 return "%s %s. %s" % (self.menu.slug, self.order, self.title)
308+
309+class AuditRecord(models.Model):
310+ audit_date = models.DateTimeField("Date", auto_now_add=True)
311+ user = models.ForeignKey(User, null=True)
312+ app_name = models.CharField("Application", max_length = 50)
313+ model_name = models.CharField("Model", max_length = 50)
314+ model_id = models.PositiveIntegerField("Model ID")
315+ AUDIT_CREATE = 0
316+ AUDIT_MODIFY = 1
317+ AUDIT_DELETE = 2
318+ AUDIT_CHANGE_TYPES = (
319+ (AUDIT_CREATE, 'create'),
320+ (AUDIT_MODIFY, 'modify'),
321+ (AUDIT_DELETE, 'delete'),
322+ )
323+ change_type = models.PositiveIntegerField("Change Type", choices = AUDIT_CHANGE_TYPES)
324+
325+ class Meta:
326+ ordering = ['-audit_date']
327+ verbose_name = "Audit Record"
328+
329+ def __unicode__(self):
330+ return "%s.%s[%s] @ %s" % (self.app_name, self.model_name, self.model_id, self.audit_date)
331+
332+class AuditChange(models.Model):
333+ record = models.ForeignKey(AuditRecord, related_name='changes')
334+ field_name = models.CharField("Field", max_length = 50)
335+ old_val = models.CharField("From", max_length = 255, blank=True, null=True)
336+ new_val = models.CharField("To", max_length = 255, blank=True, null=True)
337+
338+ class Meta:
339+ verbose_name = "Audit Change"
340+
341+ def __unicode__(self):
342+ return "%s: %s -> %s" % (self.field_name, self.old_val, self.new_val)
343
344=== modified file 'summit/schedule/models/agendamodel.py'
345--- summit/schedule/models/agendamodel.py 2012-01-23 01:18:55 +0000
346+++ summit/schedule/models/agendamodel.py 2013-02-24 22:47:21 +0000
347@@ -20,6 +20,9 @@
348 from summit.schedule.models.roommodel import Room
349 from summit.schedule.models.meetingmodel import Meeting
350
351+from common.audit import auditSave, auditDelete
352+from django.db.models.signals import pre_save, pre_delete
353+
354 __all__ = (
355 'Agenda',
356 )
357@@ -48,3 +51,5 @@
358
359 def __unicode__(self):
360 return "%s" % self.meeting
361+pre_save.connect(auditSave, sender=Agenda)
362+pre_delete.connect(auditDelete, sender=Agenda)
363
364=== modified file 'summit/schedule/models/meetingmodel.py'
365--- summit/schedule/models/meetingmodel.py 2013-02-20 13:57:06 +0000
366+++ summit/schedule/models/meetingmodel.py 2013-02-24 22:47:21 +0000
367@@ -33,6 +33,9 @@
368
369 from summit.schedule.autoslug import AutoSlugMixin
370
371+from common.audit import auditSave, auditDelete
372+from django.db.models.signals import pre_save, pre_delete
373+
374 __all__ = (
375 'Meeting',
376 )
377@@ -601,3 +604,5 @@
378 print "-- could not schedule %s in %s at %s (%s)" % (self, room, slot, e)
379
380 return False
381+pre_save.connect(auditSave, sender=Meeting)
382+pre_delete.connect(auditDelete, sender=Meeting)
383
384=== modified file 'summit/schedule/models/summitmodel.py'
385--- summit/schedule/models/summitmodel.py 2013-02-21 23:10:17 +0000
386+++ summit/schedule/models/summitmodel.py 2013-02-24 22:47:21 +0000
387@@ -40,6 +40,9 @@
388 from summit.schedule.fields import NameField
389 from summit.common import launchpad
390
391+from common.audit import auditSave, auditDelete
392+from django.db.models.signals import pre_save, pre_delete
393+
394 __all__ = (
395 'Summit',
396 'SummitSprint',
397@@ -512,6 +515,8 @@
398 if attendee is not None:
399 return self.lead_set.filter(lead=attendee).exists()
400 return False
401+pre_save.connect(auditSave, sender=Summit)
402+pre_delete.connect(auditDelete, sender=Summit)
403
404
405 class SummitSprint(models.Model):
406
407=== modified file 'summit/settings.py'
408--- summit/settings.py 2013-02-04 23:22:10 +0000
409+++ summit/settings.py 2013-02-24 22:47:21 +0000
410@@ -108,6 +108,7 @@
411 'django.middleware.locale.LocaleMiddleware',
412 'django.contrib.sessions.middleware.SessionMiddleware',
413 'django.contrib.auth.middleware.AuthenticationMiddleware',
414+ 'common.audit.CaptureRequestUser',
415 )
416
417 ROOT_URLCONF = 'summit.urls'

Subscribers

People subscribed via source and target branches