django-adminaudit-0.3.3/0000775000175000017500000000000012044424657016176 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/0000775000175000017500000000000012044424657020315 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/management/0000775000175000017500000000000012044424657022431 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/management/commands/0000775000175000017500000000000012044424657024232 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/management/commands/__init__.py0000664000175000017500000000231212044424630026330 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). from datetime import date, timedelta, time, datetime from django.core.management.base import BaseCommand from django.contrib.auth.models import User from adminaudit.models import AuditLog class AdminAuditBaseCommand(BaseCommand): def data_for_user(self, user_id, auditlogs): username = User.objects.get(pk=user_id).username caption = "{0}'s changes".format(username) return { 'auditlogs': auditlogs.filter(user_id=user_id), 'caption': caption, 'underline': "=" * len(caption), } def report_date(self): return date.today() - timedelta(days=1) def context_data(self): yesterday = self.report_date() auditlogs = AuditLog.objects.filter(created_at__range=( yesterday, datetime.combine(yesterday, time.max))) users = [] for value in auditlogs.values('user_id').distinct(): users.append(self.data_for_user(value['user_id'], auditlogs)) return { 'report_for_date': yesterday, 'users': users, } django-adminaudit-0.3.3/adminaudit/management/commands/adminaudit_email_report.py0000664000175000017500000000260712044424630031461 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). from operator import itemgetter from django.conf import settings from django.template import Context from django.template.loader import get_template from django.core.mail import send_mail from adminaudit.management.commands import AdminAuditBaseCommand class Command(AdminAuditBaseCommand): help = "Send out an email reporting the latest admin tasks performed." def handle(self, *args, **options): template = get_template("adminaudit/report.txt") recipients = getattr(settings, 'ADMINAUDIT_EMAILS_RECIPIENTS', map(itemgetter(1), settings.ADMINS)) subject = getattr(settings, 'ADMINAUDIT_SUMMARY_SUBJECT', 'Admin Audit Summary') from_email = getattr(settings, 'ADMINAUDIT_EMAIL_FROM', settings.DEFAULT_FROM_EMAIL) if not recipients: print "No admin audit summary recipients configured." context = self.context_data() for recipient in recipients: context['recipient'] = recipient print "Sending e-email to", recipient, send_mail(subject, template.render(Context(context)), from_email, [recipient], fail_silently=False) print " done" django-adminaudit-0.3.3/adminaudit/management/commands/adminaudit_report.py0000664000175000017500000000107612044424630030311 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). from django.template import Context from django.template.loader import get_template from adminaudit.management.commands import AdminAuditBaseCommand class Command(AdminAuditBaseCommand): help = "Report the latest admin tasks performed." def handle(self, *args, **kwargs): template = get_template("adminaudit/report.txt") context = Context(self.context_data()) print template.render(context) django-adminaudit-0.3.3/adminaudit/management/__init__.py0000664000175000017500000000022112044424630024524 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). django-adminaudit-0.3.3/adminaudit/templates/0000775000175000017500000000000012044424657022313 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/templates/admin/0000775000175000017500000000000012044424657023403 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/templates/admin/adminaudit/0000775000175000017500000000000012044424657025522 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/templates/admin/adminaudit/auditlog/0000775000175000017500000000000012044424657027332 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/templates/admin/adminaudit/auditlog/change_form.html0000664000175000017500000000226212044424630032461 0ustar ricardoricardo00000000000000{% extends "admin/change_form.html" %} {# Copyright 2010-2012 Canonical Ltd. This software is licensed under #} {# the GNU Lesser General Public License version 3 (see the file LICENSE). #} {% load i18n %} {% block title %}View audit log| {% trans 'Django site admin' %}{% endblock %} {% block content_title %}

View audit log

{% endblock %} {% block content %}
User {{ original.user }}
Model {{ original.model }}
Representation: {{ original.representation }}
Change {{ original.change }}
Change date {{ original.created_at }}
Change values
New values Old values
{{ new }}
{{ old }}
{% endblock %} django-adminaudit-0.3.3/adminaudit/templates/admin/base_site.html0000664000175000017500000000154212044424630026220 0ustar ricardoricardo00000000000000{% extends "admin/base.html" %} {# Copyright 2010-2012 Canonical Ltd. This software is licensed under #} {# the GNU Lesser General Public License version 3 (see the file LICENSE). #} {% load i18n %} {% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %} {% block footer %} {{ block.super }} {% endblock %} {% block branding %}

{% trans 'Django administration' %}

{% endblock %} {% block nav-global %}{% endblock %} django-adminaudit-0.3.3/adminaudit/templates/adminaudit/0000775000175000017500000000000012044424657024432 5ustar ricardoricardo00000000000000django-adminaudit-0.3.3/adminaudit/templates/adminaudit/report.txt0000664000175000017500000000105512044424630026476 0ustar ricardoricardo00000000000000{# Copyright 2010-2012 Canonical Ltd. This software is licensed under #} {# the GNU Lesser General Public License version 3 (see the file LICENSE). #} {% autoescape off %} Following is the summary of all changes made to the database via admin interface. {% for user in users %} {{ user.caption }} {{ user.underline }} {% for auditlog in user.auditlogs %}{{ auditlog.created_at }}: {{ auditlog.change }} {{ auditlog.model }} ({{ auditlog.representation }}) {% endfor %} {% endfor %} This report was generated for {{ report_for_date }}. {% endautoescape %} django-adminaudit-0.3.3/adminaudit/__init__.py0000664000175000017500000000142112044424630022413 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). def audit_install(): # import here to avoid triggering django imports during package import from django.contrib.admin import site from .models import AdminAuditMixin, AuditLog for model, model_admin in site._registry.items(): if (model is AuditLog or isinstance(model_admin, AdminAuditMixin)): # Do not mingle with our own model continue site.unregister(model) new_model_admin = type('new_model_admin', (AdminAuditMixin, model_admin.__class__), model_admin.__dict__) site.register(model, new_model_admin) django-adminaudit-0.3.3/adminaudit/admin.py0000664000175000017500000000337612044424630021757 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under the # GNU Lesser General Public License version 3 (see the file LICENSE). from django.http import Http404 from django.contrib import admin from django.utils import simplejson from adminaudit.models import AuditLog class AuditLogAdmin(admin.ModelAdmin): list_display = ('created_at', 'username', 'model', 'change') list_filter = ('username', 'created_at', 'change', 'model') def has_add_permission(self, request, obj=None): return False def delete_view(self, request, object_id, extra_context=None): raise Http404 def change_view(self, request, object_id, extra_context=None): if request.method == 'POST': raise Http404 audit_log = AuditLog.objects.get(pk=object_id) if extra_context is None: extra_context = {} if audit_log.change == 'update': decoded = simplejson.loads(audit_log.values) new_json = simplejson.dumps(decoded['new'], indent=2, sort_keys=True) old_json = simplejson.dumps(decoded['old'], indent=2, sort_keys=True) extra_context['new'] = new_json extra_context['old'] = old_json elif audit_log.change == 'delete': extra_context['new'] = '' extra_context['old'] = audit_log.values elif audit_log.change == 'create': extra_context['new'] = audit_log.values extra_context['old'] = '' return super(AuditLogAdmin, self).change_view( request, object_id, extra_context=extra_context) def get_actions(self, request): return [] admin.site.register(AuditLog, AuditLogAdmin) django-adminaudit-0.3.3/adminaudit/models.py0000664000175000017500000001004512044424630022141 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). from django.core import serializers from django.db import models from django.db.models.fields.files import FileField from django.utils import simplejson class AuditLog(models.Model): """ Records of all changes made via Django admin interface. """ username = models.CharField(max_length=255) user_id = models.IntegerField() model = models.CharField(max_length=255) change = models.CharField(max_length=100) representation = models.CharField(max_length=255) values = models.TextField() created_at = models.DateTimeField(auto_now_add=True) @classmethod def create(cls, user, obj, change, new_object=None): assert change in ['create', 'update', 'delete'] values = serializers.serialize("json", [obj]) # json[0] is for removing outside list, this serialization is only for # complete separate objects, the list is unnecessary json = simplejson.loads(values)[0] if new_object: values_new = serializers.serialize("json", [new_object]) json_new = simplejson.loads(values_new)[0] json = {'new': json_new, 'old': json} if change == 'delete': file_fields = [f for f in obj._meta.fields if isinstance(f, FileField)] if len(file_fields) > 0: json['files'] = {} for file_field in file_fields: field_name = file_field.name file = getattr(obj, field_name) if file.name: json['files'][file.name] = file.read().encode('base64') values_pretty = simplejson.dumps(json, indent=2, sort_keys=True) return cls.objects.create( username=user.username, user_id=user.id, model=str(obj._meta), values=values_pretty, representation=unicode(obj), change=change, ) class AdminAuditMixin(object): def _flatten(self, lst): result = [] for item in lst: if isinstance(item, list): result.extend(self._flatten(item)) else: result.append(item) return result def _collect_deleted_objects(self, obj): result = [] try: # This is for Django up to 1.2 from django.db.models.query_utils import CollectedObjects seen_objs = CollectedObjects() obj._collect_sub_objects(seen_objs) for cls, subobjs in seen_objs.iteritems(): for subobj in subobjs.values(): result.append(subobj) except ImportError: # Django 1.3 solution, those imports needs to be here, because # otherwise they will fail on Django < 1.3. from django.contrib.admin.util import NestedObjects from django.db import router using = router.db_for_write(obj) collector = NestedObjects(using=using) collector.collect([obj]) result = self._flatten(collector.nested()) return result def log_addition(self, request, obj, *args, **kwargs): AuditLog.create(request.user, obj, 'create') super(AdminAuditMixin, self).log_addition(request, obj, *args, **kwargs) def log_deletion(self, request, obj, *args, **kwargs): for subobj in self._collect_deleted_objects(obj): AuditLog.create(request.user, subobj, 'delete') super(AdminAuditMixin, self).log_deletion(request, obj, *args, **kwargs) def save_model(self, request, new_obj, form, change): if change: # This is so that we'll get the values of the object before the # change old_obj = new_obj.__class__.objects.get(pk=new_obj.pk) AuditLog.create(request.user, old_obj, 'update', new_object=new_obj) super(AdminAuditMixin, self).save_model( request, new_obj, form, change) django-adminaudit-0.3.3/adminaudit/schema.py0000664000175000017500000000127412044424630022122 0ustar ricardoricardo00000000000000# configglue schema to enable projects using configglue to use adminaudit # this schema represents all adminaudit available configuration settings from configglue.schema import ( ListOption, Schema, Section, StringOption, ) class AdminAuditSchema(Schema): class adminaudit(Section): adminaudit_emails_recipients = ListOption( item=StringOption(), help='List of email addresss to send reports to.') adminaudit_summary_subject = StringOption( default='Admin Audit Summary', help='Email report subject.') adminaudit_email_from = StringOption( help='Email address from which to send reports.') django-adminaudit-0.3.3/adminaudit/tests.py0000664000175000017500000003025612044424630022026 0ustar ricardoricardo00000000000000# Copyright 2010-2012 Canonical Ltd. This software is licensed under # the GNU Lesser General Public License version 3 (see the file LICENSE). import os.path import sys from cStringIO import StringIO from random import choice from string import ascii_letters from datetime import date, timedelta from django.test import TestCase from django.db.models import Model from django.conf import settings from django.core import mail from django.core.files import File from django.contrib.admin import ModelAdmin, site from django.contrib.auth.models import User, Permission from django.utils import simplejson from mock import Mock, patch from adminaudit import audit_install from adminaudit.models import AuditLog from adminaudit.management.commands.adminaudit_email_report import ( Command as EmailCommand) from adminaudit.management.commands.adminaudit_report import ( Command as CLICommand) from example_app.models import Post class BaseTestCase(TestCase): def setUp(self): User.objects.filter(username='test').delete() AuditLog.objects.all().delete() self.user = User.objects.create(username='test', is_active=True, is_staff=True, is_superuser=True) self.user.set_password('test') self.user.save() self.client.login(username='test', password='test') class AuditAdminEmailReportTestCase(TestCase): def create_and_log_in_user(self, **kwargs): defaults = { 'username': "".join(choice(ascii_letters) for x in range(5)), 'is_staff': True, } defaults.update(kwargs) user = User.objects.create(**defaults) user.set_password('test') user.save() # Give access to AuditLog and Post for perm in ['change_auditlog', 'add_post', 'change_post', 'delete_post']: user.user_permissions.add(Permission.objects.get(codename=perm)) user.save() self.client.login(username=user.username, password='test') return user def test_when_logging_in_to_admin_you_can_see_audit_log_objects(self): self.create_and_log_in_user() r = self.client.get('/admin/') self.assertContains(r, 'Audit logs') r = self.client.get('/admin/adminaudit/') self.assertEqual(200, r.status_code) def test_even_superuser_can_not_change_audit_log_object(self): user = self.create_and_log_in_user(is_superuser=True) auditlog_id = AuditLog.create(user, user, 'create').id url = '/admin/adminaudit/auditlog/{0}/'.format(auditlog_id) r = self.client.post(url, { 'values': 'test', 'representation': 'test', 'change': 'create', 'model': 'model', 'user': user.pk, }) auditlog = AuditLog.objects.get(pk=auditlog_id) self.assertNotEqual(auditlog.values, 'test') self.assertEquals(404, r.status_code) def test_even_superuser_can_not_delete_audit_log_objects(self): user = self.create_and_log_in_user(is_superuser=True) auditlog_id = AuditLog.create(user, user, 'create').id url = '/admin/adminaudit/auditlog/{0}/delete/'.format(auditlog_id) r = self.client.post(url, {'post': 'yes'}) self.assertEquals(AuditLog.objects.filter(pk=auditlog_id).count(), 1) self.assertEquals(404, r.status_code) def test_js_for_removing_change_link_from_index_page_is_present(self): self.create_and_log_in_user(is_superuser=True) r = self.client.get('/admin/') self.assertContains(r, 'a.parentNode.removeChild(a)') class AdminIntegrationTestCase(BaseTestCase): def assertOneLogCreated(self, change): logs = AuditLog.objects.filter(username=self.user.username) self.assertEquals(1, logs.count()) log = logs[0] self.assertEquals(change, log.change) def test_adding_new_object_via_admin_interface_is_saved_by_audit(self): r = self.client.post('/admin/auth/user/add/', { 'username': "test_user", 'password1': "test", 'password2': "test", }) # Redirect after object creation self.assertEquals(302, r.status_code) self.assertOneLogCreated('create') def test_changing_object_via_admin_is_saved_by_audit(self): r = self.client.get('/admin/auth/user/{0}/'.format(self.user.pk)) data = r.context['adminform'].form.initial data.update({ 'first_name': "First", 'last_login_0': str(data['last_login'].date()), 'last_login_1': data['last_login'].strftime("%H:%M:%S"), 'date_joined_0': str(data['date_joined'].date()), 'date_joined_1': data['date_joined'].strftime("%H:%M:%S"), }) r = self.client.post( '/admin/auth/user/{0}/'.format(self.user.pk), data) self.assertEquals(302, r.status_code) self.assertOneLogCreated('update') def test_deleting_object_via_admin_is_saved_by_audit(self): user = User.objects.create_user('newuser', 'new@example.com') r = self.client.post('/admin/auth/user/{0}/delete/'.format(user.pk), { 'post': 'yes', }) self.assertEquals(302, r.status_code) self.assertOneLogCreated('delete') def test_cascading_deleted_objects_also_receive_audit_log(self): user = User.objects.create_user('newuser', 'new@example.com') Post.objects.create(author=user, title='foo', body='bar') self.client.post('/admin/auth/user/{0}/delete/'.format(user.pk), { 'post': 'yes', }) # Check that delete cascaded to the related Post object self.assertEqual(0, Post.objects.count()) # Check that an AuditLog object was created for each self.assertEqual(2, AuditLog.objects.count()) def test_nested_cascading_deleted_objects(self): user = User.objects.create_user('newuser', 'new@example.com') post1 = Post.objects.create(author=user, title='foo', body='bar') Post.objects.create(author=user, title='bar', body='baz', related=post1) self.client.post('/admin/auth/user/{0}/delete/'.format(user.pk), { 'post': 'yes', }) # Check that delete cascaded to the related Post objects self.assertEqual(0, Post.objects.count()) # Check that an AuditLog object was created for each self.assertEqual(3, AuditLog.objects.count()) class AuditLogAdminChangeViewTestCase(BaseTestCase): def test_display_change_view_for_create_log(self): auditlog = AuditLog.create(self.user, self.user, 'create') r = self.client.get( '/admin/adminaudit/auditlog/{0}/'.format(auditlog.pk)) self.assertContains(r, 'test') self.assertContains(r, 'auth.user') self.assertTrue(r.context['new']) self.assertFalse(r.context['old']) def test_display_change_view_for_update_log(self): auditlog = AuditLog.create(self.user, self.user, 'update', self.user) r = self.client.get( '/admin/adminaudit/auditlog/{0}/'.format(auditlog.pk)) self.assertTrue(r.context['new']) self.assertTrue(r.context['old']) def test_display_change_view_for_delete_log(self): auditlog = AuditLog.create(self.user, self.user, 'delete') r = self.client.get( '/admin/adminaudit/auditlog/{0}/'.format(auditlog.pk)) self.assertFalse(r.context['new']) self.assertTrue(r.context['old']) def test_display_change_list(self): AuditLog.create(self.user, self.user, 'delete') r = self.client.get('/admin/adminaudit/auditlog/') self.assertNotContains(r, '