django-adminaudit-0.3.2/0000775000175000017500000000000011746507402015747 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/PKG-INFO0000664000175000017500000000120711746507402017044 0ustar lukaszlukasz00000000000000Metadata-Version: 1.1 Name: django-adminaudit Version: 0.3.2 Summary: Extends Django's admin logging capabilities Home-page: https://launchpad.net/django-adminaudit Author: Łukasz Czyżykowski Author-email: lukasz.czyzykowski@canonical.com License: LGPLv3 Description: UNKNOWN Platform: UNKNOWN Classifier: Framework :: Django Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules django-adminaudit-0.3.2/adminaudit/0000775000175000017500000000000011746507402020066 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/models.py0000664000175000017500000000405211737026766021735 0ustar lukaszlukasz00000000000000# 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.utils import simplejson from django.db.models.fields.files import FileField 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, ) django-adminaudit-0.3.2/adminaudit/templates/0000775000175000017500000000000011746507402022064 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/templates/admin/0000775000175000017500000000000011746507402023154 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/templates/admin/base_site.html0000664000175000017500000000154211740515574026005 0ustar lukaszlukasz00000000000000{% 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.2/adminaudit/templates/admin/adminaudit/0000775000175000017500000000000011746507402025273 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/templates/admin/adminaudit/auditlog/0000775000175000017500000000000011746507402027103 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/templates/admin/adminaudit/auditlog/change_form.html0000664000175000017500000000226211740767212032244 0ustar lukaszlukasz00000000000000{% 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.2/adminaudit/templates/adminaudit/0000775000175000017500000000000011746507402024203 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/templates/adminaudit/report.txt0000664000175000017500000000105511740515574026263 0ustar lukaszlukasz00000000000000{# 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.2/adminaudit/management/0000775000175000017500000000000011746507402022202 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/management/commands/0000775000175000017500000000000011746507402024003 5ustar lukaszlukasz00000000000000django-adminaudit-0.3.2/adminaudit/management/commands/adminaudit_email_report.py0000664000175000017500000000260711740515573031245 0ustar lukaszlukasz00000000000000# 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.2/adminaudit/management/commands/adminaudit_report.py0000664000175000017500000000107611740515573030075 0ustar lukaszlukasz00000000000000# 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.2/adminaudit/management/commands/__init__.py0000664000175000017500000000231211740515573026114 0ustar lukaszlukasz00000000000000# 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.2/adminaudit/management/__init__.py0000664000175000017500000000022111737027306024306 0ustar lukaszlukasz00000000000000# 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.2/adminaudit/tests.py0000664000175000017500000003025611746506121021605 0ustar lukaszlukasz00000000000000# 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, '