django-adminaudit-0.3.3/ 0000775 0001750 0001750 00000000000 12044424657 016176 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/ 0000775 0001750 0001750 00000000000 12044424657 020315 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/management/ 0000775 0001750 0001750 00000000000 12044424657 022431 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/management/commands/ 0000775 0001750 0001750 00000000000 12044424657 024232 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/management/commands/__init__.py 0000664 0001750 0001750 00000002312 12044424630 026330 0 ustar ricardo ricardo 0000000 0000000 # 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.py 0000664 0001750 0001750 00000002607 12044424630 031461 0 ustar ricardo ricardo 0000000 0000000 # 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.py 0000664 0001750 0001750 00000001076 12044424630 030311 0 ustar ricardo ricardo 0000000 0000000 # 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__.py 0000664 0001750 0001750 00000000221 12044424630 024524 0 ustar ricardo ricardo 0000000 0000000 # 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/ 0000775 0001750 0001750 00000000000 12044424657 022313 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/templates/admin/ 0000775 0001750 0001750 00000000000 12044424657 023403 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/templates/admin/adminaudit/ 0000775 0001750 0001750 00000000000 12044424657 025522 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/templates/admin/adminaudit/auditlog/ 0000775 0001750 0001750 00000000000 12044424657 027332 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/templates/admin/adminaudit/auditlog/change_form.html 0000664 0001750 0001750 00000002262 12044424630 032461 0 ustar ricardo ricardo 0000000 0000000 {% 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.html 0000664 0001750 0001750 00000001542 12044424630 026220 0 ustar ricardo ricardo 0000000 0000000 {% 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/ 0000775 0001750 0001750 00000000000 12044424657 024432 5 ustar ricardo ricardo 0000000 0000000 django-adminaudit-0.3.3/adminaudit/templates/adminaudit/report.txt 0000664 0001750 0001750 00000001055 12044424630 026476 0 ustar ricardo ricardo 0000000 0000000 {# 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__.py 0000664 0001750 0001750 00000001421 12044424630 022413 0 ustar ricardo ricardo 0000000 0000000 # 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.py 0000664 0001750 0001750 00000003376 12044424630 021757 0 ustar ricardo ricardo 0000000 0000000 # 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.py 0000664 0001750 0001750 00000010045 12044424630 022141 0 ustar ricardo ricardo 0000000 0000000 # 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.py 0000664 0001750 0001750 00000001274 12044424630 022122 0 ustar ricardo ricardo 0000000 0000000 # 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.py 0000664 0001750 0001750 00000030256 12044424630 022026 0 ustar ricardo ricardo 0000000 0000000 # 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, '