Merge lp:~mhall119/summit/add-auditing into lp:summit
- add-auditing
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Summit Hackers | Pending | ||
Review via email: mp+150249@code.launchpad.net |
Commit message
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' |