Compare commits

...

64 commits

Author SHA1 Message Date
Alieh Rymašeŭski
a0a0726982 Bump version to 0.7.2 2020-11-18 13:29:50 +03:00
Alieh Rymašeŭski
ff6349a89a Order filter by field alphabetically 2020-11-18 13:29:29 +03:00
Alieh Rymašeŭski
8b97cc2acb Bump version to 0.7.1 2020-11-12 18:04:00 +03:00
Alieh Rymašeŭski
5dcb069bb8 Add a short filter by actor__isnull 2020-11-12 18:03:27 +03:00
Alieh Rymašeŭski
d7a6496ad8 Bump version to 0.7.0 2020-11-12 17:59:02 +03:00
Alieh Rymašeŭski
df16b2a8da Changes for Jazzband (#269)
* Update repository references to Jazzband
Issue #268

* Add Jazzband badge to README
Issue #268

* Add Jazzband contribution guideline
Issue #268

Cherry-picking 31418d54f2
2020-11-12 16:51:20 +03:00
Alieh Rymašeŭski
be82018266 Use more generic .pk to get primary key instead of .id
Fixes #140

Cherry-picking fdf6ed7149c2731e3bd9144f33a5d70418e05889
2020-11-12 16:50:12 +03:00
Alieh Rymašeŭski
de693dd092 Management command improvements
Cherry-picking f4edfc0592
2020-11-12 16:50:12 +03:00
Alieh Rymašeŭski
6379683f77 Remove stale code
Cherry-picking f14f6b34ee
2020-11-12 16:41:42 +03:00
Alieh Rymašeŭski
c346adb8b5 Code improvements
Cherry-picking 469fe362de
2020-11-12 16:40:48 +03:00
Alieh Rymašeŭski
701f867a04 Remove a deprecated alias 2020-11-12 16:32:41 +03:00
Alieh Rymašeŭski
0ca00faafc Simplify field.base_field.choices check 2020-11-12 16:21:31 +03:00
Alieh Rymašeŭski
cb1fefb793 Allow setting database parameters through env 2020-11-12 16:21:09 +03:00
Alieh Rymašeŭski
536a841bf3 Remove Python 2 cruft 2020-11-12 16:15:32 +03:00
Alieh Rymašeŭski
08a7b82acc Remove version constraints on test requirements 2020-11-12 16:15:25 +03:00
Alieh Rymašeŭski
8d2bb0f319 Drop support for outdated Django versions 2020-11-12 16:15:12 +03:00
Alieh Rymašeŭski
3ac4311ae4 Align Travis test matrix with tox.ini 2020-11-12 16:15:12 +03:00
Alieh Rymašeŭski
917b490ee4 Bump copyright year
Cherry-picking 4e7c640ba0
2020-11-12 16:15:12 +03:00
Alieh Rymašeŭski
47a268eef9 Clean up project structure
Cherry-picking ee8a700b1b
2020-11-12 16:15:05 +03:00
Alieh Rymašeŭski
ca29848d78 Remove compatibility code for old Django versions 2020-11-12 12:31:23 +03:00
Alieh Rymašeŭski
a6cea38c9e Bump version to 0.6.10 2020-06-11 17:05:29 +03:00
Alieh Rymašeŭski
33a35e841b Export the logic to limit database query time 2020-06-11 17:05:29 +03:00
Alieh Rymašeŭski
a11d1a3e46 Bump version to 0.6.9 2020-06-10 15:40:07 +03:00
Alieh Rymašeŭski
45a27ec1d6 Support Django 3.0 2020-06-10 15:31:41 +03:00
Alieh Rymašeŭski
e6f3f12bae Relax requirements for tests 2020-06-10 15:21:48 +03:00
Alieh Rymašeŭski
28a60af5f3 Move Travis-specific requirements into own file 2020-06-10 15:21:48 +03:00
Alieh Rymašeŭski
fd169771df Support running tox -p 2020-06-10 15:21:48 +03:00
Alieh Rymašeŭski
d97ac056d4 Remove unused requirements.txt
tox installs the dependencies specified in setup.py.
2020-06-10 14:20:39 +03:00
Alieh Rymašeŭski
33c35b42db Support python 3.8 2020-06-10 14:08:33 +03:00
Alieh Rymašeŭski
0f3c7b430f Bump version to 0.6.8 2020-04-22 17:57:21 +03:00
Alieh Rymašeŭski
bf8ba7a0be Add admin filter by timestamp
It's backed by django-admin-rangefilter if able.
2020-04-22 17:42:53 +03:00
Alieh Rymašeŭski
2b536fc87e Bump version to 0.6.7 2020-04-22 16:09:27 +03:00
Alieh Rymašeŭski
106417c684 Display timestamps in server timezone 2020-04-22 16:03:01 +03:00
Alieh Rymašeŭski
358971aafe Bump version to 0.6.6 2020-04-17 12:54:54 +03:00
Alieh Rymašeŭski
f58b3d7685 Introduce admin filter by changed field 2020-04-17 12:52:56 +03:00
Alieh Rymašeŭski
5cd55ac38c Bump version to 0.6.5 2020-04-07 12:03:09 +03:00
Alieh Rymašeŭski
8397754a20 Replace dumb paginator with a time-limited one 2020-04-07 11:55:55 +03:00
Alieh Rymašeŭski
da49432924 Bump version to 0.6.4 2020-04-07 11:23:48 +03:00
Alieh Rymašeŭski
3af06e13c7 Add a missing line to manifest 2020-04-07 11:17:39 +03:00
Alieh Rymašeŭski
ae57b0c322 Merge upstream changes from jjkester/master
Conflicts:
    setup.py  - conflicting version bump, expected
    src/auditlog_tests/router.py - removed by them
    tox.ini - upstream extended test matrix with dj21 and dj22, we did
              the same but dropped older versions, keepign our variant
2020-04-07 11:13:45 +03:00
Alieh Rymašeŭski
bde49bdb4f Allow newer versions of dateutil 2019-11-25 15:49:00 +03:00
Alieh Rymašeŭski
c66b36c700 Bump version to 0.6.2 2019-08-22 18:24:46 +03:00
Alieh Rymašeŭski
2ae401a04f Shave off two SELECT COUNT(*) from admin page 2019-08-22 18:24:10 +03:00
Alieh Rymašeŭski
5ba554af56 Bump version to 0.6.1 2019-08-22 18:06:48 +03:00
Alieh Rymašeŭski
df2bf0a05c Replace timestamp index with timestamp+id index
This change is supposed to improve performance of admin view.
2019-08-22 18:05:39 +03:00
Alieh Rymašeŭski
05e6b179fd Bump version to 0.6.0 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
07b38a9345 Update references to the maintainer 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
5784247180 Update requirements section in the docs
We've dropped support for Python2 and unsupported versions of Python
and Django.
2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
2a43cff96f Add python3.7 to tox and classifiers 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
b16b1a0df3 Drop Django 1.9 compatibility 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
9152d225bb Remove obsolete versions from setup and tox 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
c53b766132 Drop python2.7 support
Our dependency jsonfield broke compatibility with python2.7 recently,
and having no real reasons to support python2.7 we just drop it now.
2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
e35d0f4194 Bump version to 0.5.3 2019-08-21 19:41:02 +03:00
Alieh Rymašeŭski
e60876ae14 Add index for timestamp 2019-08-21 19:30:25 +03:00
Alieh Rymašeŭski
5dbea8a9a1 Bump version to 0.5.2 2019-05-17 12:41:31 +03:00
Alieh Rymašeŭski
cfbc588cc1 Query ContentTypes instead of distinct LogEntry
SELECT DISTINCT app_label, model FROM log_entry is a very expensive
request for longer logs, while we can always get the list of all
tracked models straight from the registry.

This new approach has two downsides:
1. It only provides filters for currently tracked models.
2. It can list such filter options that don't have any log entries.
2019-05-17 12:38:39 +03:00
Alieh Rymašeŭski
ee6bb33bc9 Bump version to 0.5.1 2019-05-16 17:24:13 +03:00
Alieh Rymašeŭski
c9c97b6861 Improve admin list performance 2019-05-16 17:23:28 +03:00
Alieh Rymašeŭski
5f5cc7f7e9 Bump version to 0.5.0 2019-05-11 18:43:36 +03:00
Alieh Rymašeŭski
a5381b6195 Move signal management to a context manager
This change allows setting the same signals when the request is not
present, i.e. in a celery task.
2019-05-11 18:25:08 +03:00
Alieh Rymašeŭski
2dc0ac43b5 Use get_user_model 2019-05-11 18:16:00 +03:00
Alieh Rymašeŭski
aa28009d3b Configure tests for Django 2.2 2019-05-11 18:16:00 +03:00
Alieh Rymašeŭski
03b8616dac Configure tests for Django 2.1 2019-05-11 18:15:58 +03:00
Alieh Rymašeŭski
62c1e676cc Bump version 2019-03-28 14:29:31 +03:00
55 changed files with 670 additions and 1126 deletions

90
.gitignore vendored
View file

@ -1,12 +1,82 @@
*.db
*.egg-info
*.log
*.pot
*.pyc
.idea
.project
.pydevproject
.coverage
venv/
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Sphinx documentation
docs/_build/
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
### JetBrains
.idea/

View file

@ -11,44 +11,29 @@ addons:
matrix:
include:
- python: 2.7
env: TOXENV=py27-django-111
- python: 3.4
env: TOXENV=py34-django-111
- python: 3.4
env: TOXENV=py34-django-20
- python: 3.5
env: TOXENV=py35-django-111
- python: 3.5
env: TOXENV=py35-django-20
- python: 3.5
env: TOXENV=py35-django-21
- python: 3.5
env: TOXENV=py35-django-22
- python: 3.5
env: TOXENV=py35-django-30
- python: 3.6
env: TOXENV=py36-django-111
- python: 3.6
env: TOXENV=py36-django-20
- python: 3.6
env: TOXENV=py36-django-21
- python: 3.6
env: TOXENV=py36-django-22
- python: 3.6
env: TOXENV=py36-django-30
- python: 3.7
env: TOXENV=py37-django-111
- python: 3.7
env: TOXENV=py37-django-20
- python: 3.7
env: TOXENV=py37-django-21
- python: 3.7
env: TOXENV=py37-django-22
- python: 3.7
env: TOXENV=py37-django-30
- python: 3.8
env: TOXENV=py38-django-22
- python: 3.8
env: TOXENV=py38-django-30
fast_finish: true
install: pip install -r requirements-test.txt
install: pip install -r requirements.txt
script: tox
@ -59,7 +44,7 @@ deploy:
provider: pypi
# PyPI credentials supplied with environment variables from repository settings
on:
repo: jjkester/django-auditlog
repo: jazzband/django-auditlog
branch: stable
condition: $TOXENV = py36-django-20
condition: $TOXENV = py38-django-30
edge: true

3
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,3 @@
[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/)
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines).

View file

@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2015 Jan-Jelle Kester
Copyright (c) 2013-2020 Jan-Jelle Kester
Copyright (c) 2019-2020 Alieh Rymašeuski <alieh.rymasheuski@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View file

@ -1,24 +0,0 @@
# file GENERATED by distutils, do NOT edit
setup.py
src/auditlog/__init__.py
src/auditlog/admin.py
src/auditlog/apps.py
src/auditlog/compat.py
src/auditlog/diff.py
src/auditlog/filters.py
src/auditlog/middleware.py
src/auditlog/mixins.py
src/auditlog/models.py
src/auditlog/receivers.py
src/auditlog/registry.py
src/auditlog/management/__init__.py
src/auditlog/management/commands/__init__.py
src/auditlog/management/commands/auditlogflush.py
src/auditlog/migrations/0001_initial.py
src/auditlog/migrations/0002_auto_support_long_primary_keys.py
src/auditlog/migrations/0003_logentry_remote_addr.py
src/auditlog/migrations/0004_logentry_detailed_object_repr.py
src/auditlog/migrations/0005_logentry_additional_data_verbose_name.py
src/auditlog/migrations/0006_object_pk_index.py
src/auditlog/migrations/0007_object_pk_type.py
src/auditlog/migrations/__init__.py

View file

@ -1,7 +1,8 @@
django-auditlog
===============
[![Build Status](https://travis-ci.org/jjkester/django-auditlog.svg?branch=master)](https://travis-ci.org/jjkester/django-auditlog)
[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
[![Build Status](https://travis-ci.org/jazzband/django-auditlog.svg?branch=master)](https://travis-ci.org/jazzband/django-auditlog)
[![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](http://django-auditlog.readthedocs.org/en/latest/?badge=latest)
**Please remember that this app is still in development.**

View file

@ -1,3 +1,3 @@
from __future__ import unicode_literals
__version__ = '0.7.2'
default_app_config = 'auditlog.apps.AuditlogConfig'

34
auditlog/admin.py Normal file
View file

@ -0,0 +1,34 @@
from django.conf import settings
from django.contrib import admin
from django.core.paginator import Paginator
from django.utils.functional import cached_property
from .count import limit_query_time
from .models import LogEntry
from .mixins import LogEntryAdminMixin
from .filters import ShortActorFilter, ResourceTypeFilter, FieldFilter, get_timestamp_filter
class TimeLimitedPaginator(Paginator):
"""A PostgreSQL-specific paginator with a hard time limit for total count of pages."""
@cached_property
@limit_query_time(getattr(settings, 'AUDITLOG_PAGINATOR_TIMEOUT', 500), default=100000)
def count(self):
return super().count
class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin):
list_display = ['created', 'resource_url', 'action', 'msg_short', 'user_url']
search_fields = ['timestamp', 'object_repr', 'changes', 'actor__first_name', 'actor__last_name']
list_filter = ['action', ShortActorFilter, ResourceTypeFilter, FieldFilter, ('timestamp', get_timestamp_filter())]
readonly_fields = ['created', 'resource_url', 'action', 'user_url', 'msg']
fieldsets = [
(None, {'fields': ['created', 'user_url', 'resource_url']}),
('Changes', {'fields': ['action', 'msg']}),
]
list_select_related = ['actor', 'content_type']
show_full_result_count = False
paginator = TimeLimitedPaginator
admin.site.register(LogEntry, LogEntryAdmin)

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
from django.apps import AppConfig

55
auditlog/context.py Normal file
View file

@ -0,0 +1,55 @@
import contextlib
from functools import partial
import time
import threading
from django.contrib.auth import get_user_model
from django.db.models.signals import pre_save
from auditlog.models import LogEntry
threadlocal = threading.local()
@contextlib.contextmanager
def set_actor(actor, remote_addr=None):
"""Connect a signal receiver with current user attached."""
# Initialize thread local storage
threadlocal.auditlog = {
'signal_duid': ('set_actor', time.time()),
'remote_addr': remote_addr,
}
# Connect signal for automatic logging
set_actor = partial(_set_actor, user=actor, signal_duid=threadlocal.auditlog['signal_duid'])
pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False)
try:
yield
finally:
try:
auditlog = threadlocal.auditlog
except AttributeError:
pass
else:
pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog['signal_duid'])
def _set_actor(user, sender, instance, signal_duid, **kwargs):
"""Signal receiver with an extra 'user' kwarg.
This function becomes a valid signal receiver when it is curried with the actor.
"""
try:
auditlog = threadlocal.auditlog
except AttributeError:
pass
else:
if signal_duid != auditlog['signal_duid']:
return
auth_user_model = get_user_model()
if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None:
instance.actor = user
instance.remote_addr = auditlog['remote_addr']

23
auditlog/count.py Normal file
View file

@ -0,0 +1,23 @@
from django.db import connection, transaction, OperationalError
def limit_query_time(timeout, default=None):
"""A PostgreSQL-specific decorator with a hard time limit and a default return value.
Timeout in milliseconds.
Courtesy of https://medium.com/@hakibenita/optimizing-django-admin-paginator-53c4eb6bfca3
"""
def decorator(function):
def _limit_query_time(*args, **kwargs):
with transaction.atomic(), connection.cursor() as cursor:
cursor.execute('SET LOCAL statement_timeout TO %s;', (timeout,))
try:
return function(*args, **kwargs)
except OperationalError:
return default
return _limit_query_time
return decorator

View file

@ -1,10 +1,8 @@
from __future__ import unicode_literals
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model, NOT_PROVIDED, DateTimeField
from django.utils import timezone
from django.utils.encoding import smart_text
from django.utils.encoding import smart_str
def track_field(field):
@ -27,10 +25,6 @@ def track_field(field):
if getattr(field, 'remote_field', None) is not None and field.remote_field.model == LogEntry:
return False
# 1.8 check
elif getattr(field, 'rel', None) is not None and field.rel.to == LogEntry:
return False
return True
@ -46,17 +40,13 @@ def get_fields_in_model(instance):
"""
assert isinstance(instance, Model)
# Check if the Django 1.8 _meta API is available
use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields)
if use_api:
return [f for f in instance._meta.get_fields() if track_field(f)]
return instance._meta.fields
return [f for f in instance._meta.get_fields() if track_field(f)]
def get_field_value(obj, field):
"""
Gets the value of a given model instance field.
:param obj: The model instance.
:type obj: Model
:param field: The field you want to find the value of.
@ -66,7 +56,7 @@ def get_field_value(obj, field):
"""
if isinstance(field, DateTimeField):
# DateTimeFields are timezone-aware, so we need to convert the field
# to its naive form before we can accuratly compare them for changes.
# to its naive form before we can accurately compare them for changes.
try:
value = field.to_python(getattr(obj, field.name, None))
if value is not None and settings.USE_TZ and not timezone.is_naive(value):
@ -75,7 +65,7 @@ def get_field_value(obj, field):
value = field.default if field.default is not NOT_PROVIDED else None
else:
try:
value = smart_text(getattr(obj, field.name, None))
value = smart_str(getattr(obj, field.name, None))
except ObjectDoesNotExist:
value = field.default if field.default is not NOT_PROVIDED else None
@ -97,9 +87,9 @@ def model_instance_diff(old, new):
"""
from auditlog.registry import auditlog
if not(old is None or isinstance(old, Model)):
if not (old is None or isinstance(old, Model)):
raise TypeError("The supplied old instance is not a valid model instance.")
if not(new is None or isinstance(new, Model)):
if not (new is None or isinstance(new, Model)):
raise TypeError("The supplied new instance is not a valid model instance.")
diff = {}
@ -135,7 +125,7 @@ def model_instance_diff(old, new):
new_value = get_field_value(new, field)
if old_value != new_value:
diff[field.name] = (smart_text(old_value), smart_text(new_value))
diff[field.name] = (smart_str(old_value), smart_str(new_value))
if len(diff) == 0:
diff = None

96
auditlog/filters.py Normal file
View file

@ -0,0 +1,96 @@
from django.apps import apps
from django.contrib.admin import SimpleListFilter
from django.contrib.admin.filters import DateFieldListFilter
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.db import connection
from django.db.models import Value
from django.db.models.functions import Concat, Cast
from auditlog.registry import auditlog
class ShortActorFilter(SimpleListFilter):
title = 'Actor'
parameter_name = 'actor'
def lookups(self, request, model_admin):
return [("null", "System"), ("not_null", "Users")]
def queryset(self, request, queryset):
value = self.value()
if value is None:
return queryset
if value == "null":
return queryset.filter(actor__isnull=True)
return queryset.filter(actor__isnull=False)
class ResourceTypeFilter(SimpleListFilter):
title = 'Resource Type'
parameter_name = 'resource_type'
def lookups(self, request, model_admin):
tracked_model_names = [
'{}.{}'.format(m._meta.app_label, m._meta.model_name)
for m in auditlog.list()
]
model_name_concat = Concat('app_label', Value('.'), 'model')
content_types = ContentType.objects.annotate(
model_name=model_name_concat,
).filter(
model_name__in=tracked_model_names,
)
return content_types.order_by('model_name').values_list('id', 'model_name')
def queryset(self, request, queryset):
if self.value() is None:
return queryset
return queryset.filter(content_type_id=self.value())
class FieldFilter(SimpleListFilter):
title = 'Field'
parameter_name = 'field'
parent = ResourceTypeFilter
def __init__(self, request, *args, **kwargs):
self.target_model = self._get_target_model(request)
super().__init__(request, *args, **kwargs)
def _get_target_model(self, request):
# the parameters consumed by previous filters aren't passed to subsequent filters,
# so we have to look into the request parameters explicitly
content_type_id = request.GET.get(self.parent.parameter_name)
if not content_type_id:
return None
return ContentType.objects.get(id=content_type_id).model_class()
def lookups(self, request, model_admin):
if connection.vendor != 'postgresql':
# filtering inside JSON is PostgreSQL-specific for now
return []
if not self.target_model:
return []
return sorted((field.name, field.name) for field in self.target_model._meta.fields)
def queryset(self, request, queryset):
if self.value() is None:
return queryset
return (
queryset.annotate(changes_json=Cast("changes", JSONField()))
.filter(**{'changes_json__{}__isnull'.format(self.value()): False})
)
def get_timestamp_filter():
"""Returns rangefilter filter class if able or a simple list filter as a fallback."""
if apps.is_installed("rangefilter"):
try:
from rangefilter.filter import DateTimeRangeFilter
return DateTimeRangeFilter
except ImportError:
pass
return DateFieldListFilter

View file

@ -0,0 +1,25 @@
from django.core.management.base import BaseCommand
from auditlog.models import LogEntry
class Command(BaseCommand):
help = "Deletes all log entries from the database."
def add_arguments(self, parser):
parser.add_argument('-y, --yes', action='store_true', default=None,
help="Continue without asking confirmation.", dest='yes')
def handle(self, *args, **options):
answer = options['yes']
if answer is None:
self.stdout.write("This action will clear all log entries from the database.")
response = input("Are you sure you want to continue? [y/N]: ").lower().strip()
answer = response == 'y'
if answer:
count, _ = LogEntry.objects.all().delete()
self.stdout.write("Deleted %d objects." % count)
else:
self.stdout.write("Aborted.")

35
auditlog/middleware.py Normal file
View file

@ -0,0 +1,35 @@
import contextlib
from auditlog.context import set_actor
@contextlib.contextmanager
def nullcontext():
"""Equivalent to contextlib.nullcontext(None) from Python 3.7."""
yield
class AuditlogMiddleware(object):
"""
Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the
user from the request (or None if the user is not authenticated).
"""
def __init__(self, get_response=None):
self.get_response = get_response
def __call__(self, request):
if request.META.get('HTTP_X_FORWARDED_FOR'):
# In case of proxy, set 'original' address
remote_addr = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0]
else:
remote_addr = request.META.get('REMOTE_ADDR')
if hasattr(request, 'user') and request.user.is_authenticated:
context = set_actor(actor=request.user, remote_addr=remote_addr)
else:
context = nullcontext()
with context:
return self.get_response(request)

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
from django.conf import settings

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations
import jsonfield.fields

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.db import migrations
import jsonfield.fields

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auditlog', '0007_object_pk_type'),
]
operations = [
migrations.AlterField(
model_name='logentry',
name='timestamp',
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='timestamp'),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auditlog', '0008_timestamp_index'),
]
operations = [
migrations.AlterIndexTogether(
name='logentry',
index_together={('timestamp', 'id')},
),
migrations.AlterField(
model_name='logentry',
name='timestamp',
field=models.DateTimeField(auto_now_add=True, verbose_name='timestamp'),
),
]

View file

@ -1,16 +1,13 @@
import json
from django import urls as urlresolvers
from django.conf import settings
try:
from django.core import urlresolvers
except ImportError:
from django import urls as urlresolvers
try:
from django.urls.exceptions import NoReverseMatch
except ImportError:
from django.core.urlresolvers import NoReverseMatch
from django.urls.exceptions import NoReverseMatch
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.timezone import localtime
from auditlog.models import LogEntry
MAX = 75
@ -18,7 +15,8 @@ MAX = 75
class LogEntryAdminMixin(object):
def created(self, obj):
return obj.timestamp.strftime('%Y-%m-%d %H:%M:%S')
return localtime(obj.timestamp).strftime('%Y-%m-%d %H:%M:%S')
created.short_description = 'Created'
def user_url(self, obj):
@ -26,12 +24,13 @@ class LogEntryAdminMixin(object):
app_label, model = settings.AUTH_USER_MODEL.split('.')
viewname = 'admin:%s_%s_change' % (app_label, model.lower())
try:
link = urlresolvers.reverse(viewname, args=[obj.actor.id])
link = urlresolvers.reverse(viewname, args=[obj.actor.pk])
except NoReverseMatch:
return u'%s' % (obj.actor)
return format_html(u'<a href="{}">{}</a>', link, obj.actor)
return 'system'
user_url.short_description = 'User'
def resource_url(self, obj):
@ -44,10 +43,11 @@ class LogEntryAdminMixin(object):
return obj.object_repr
else:
return format_html(u'<a href="{}">{}</a>', link, obj.object_repr)
resource_url.short_description = 'Resource'
def msg_short(self, obj):
if obj.action == 2:
if obj.action == LogEntry.Action.DELETE:
return '' # delete
changes = json.loads(obj.changes)
s = '' if len(changes) == 1 else 's'
@ -56,10 +56,11 @@ class LogEntryAdminMixin(object):
i = fields.rfind(' ', 0, MAX)
fields = fields[:i] + ' ..'
return '%d change%s: %s' % (len(changes), s, fields)
msg_short.short_description = 'Changes'
def msg(self, obj):
if obj.action == 2:
if obj.action == LogEntry.Action.DELETE:
return '' # delete
changes = json.loads(obj.changes)
msg = '<table><tr><th>#</th><th>Field</th><th>From</th><th>To</th></tr>'
@ -69,4 +70,5 @@ class LogEntryAdminMixin(object):
msg += '</table>'
return mark_safe(msg)
msg.short_description = 'Changes'

View file

@ -1,8 +1,8 @@
from __future__ import unicode_literals
import json
import ast
import json
from dateutil import parser
from dateutil.tz import gettz
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
@ -10,13 +10,9 @@ from django.core.exceptions import FieldDoesNotExist
from django.db import models, DEFAULT_DB_ALIAS
from django.db.models import QuerySet, Q
from django.utils import formats, timezone
from django.utils.encoding import python_2_unicode_compatible, smart_text
from django.utils.six import iteritems, integer_types
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_str
from django.utils.translation import gettext_lazy as _
from jsonfield.fields import JSONField
from dateutil import parser
from dateutil.tz import gettz
class LogEntryManager(models.Manager):
@ -41,9 +37,9 @@ class LogEntryManager(models.Manager):
if changes is not None:
kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance))
kwargs.setdefault('object_pk', pk)
kwargs.setdefault('object_repr', smart_text(instance))
kwargs.setdefault('object_repr', smart_str(instance))
if isinstance(pk, integer_types):
if isinstance(pk, int):
kwargs.setdefault('object_id', pk)
get_additional_data = getattr(instance, 'get_additional_data', None)
@ -78,10 +74,10 @@ class LogEntryManager(models.Manager):
content_type = ContentType.objects.get_for_model(instance.__class__)
pk = self._get_pk_value(instance)
if isinstance(pk, integer_types):
if isinstance(pk, int):
return self.filter(content_type=content_type, object_id=pk)
else:
return self.filter(content_type=content_type, object_pk=smart_text(pk))
return self.filter(content_type=content_type, object_pk=smart_str(pk))
def get_for_objects(self, queryset):
"""
@ -98,10 +94,10 @@ class LogEntryManager(models.Manager):
content_type = ContentType.objects.get_for_model(queryset.model)
primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True))
if isinstance(primary_keys[0], integer_types):
if isinstance(primary_keys[0], int):
return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct()
elif isinstance(queryset.model._meta.pk, models.UUIDField):
primary_keys = [smart_text(pk) for pk in primary_keys]
primary_keys = [smart_str(pk) for pk in primary_keys]
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
else:
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
@ -140,7 +136,6 @@ class LogEntryManager(models.Manager):
return pk
@python_2_unicode_compatible
class LogEntry(models.Model):
"""
Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available)
@ -189,6 +184,7 @@ class LogEntry(models.Model):
ordering = ['-timestamp']
verbose_name = _("log entry")
verbose_name_plural = _("log entries")
index_together = (("timestamp", "id"))
def __str__(self):
if self.action == self.Action.CREATE:
@ -213,7 +209,7 @@ class LogEntry(models.Model):
return {}
@property
def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '):
def changes_str(self, colon=': ', arrow=' \u2192 ', separator='; '):
"""
Return the changes recorded in this log entry as a string. The formatting of the string can be customized by
setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use
@ -226,8 +222,8 @@ class LogEntry(models.Model):
"""
substrings = []
for field, values in iteritems(self.changes_dict):
substring = smart_text('{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format(
for field, values in self.changes_dict.items():
substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format(
field_name=field,
colon=colon,
old=values[0],
@ -249,7 +245,7 @@ class LogEntry(models.Model):
model_fields = auditlog.get_model_fields(model._meta.model)
changes_display_dict = {}
# grab the changes_dict and iterate through
for field_name, values in iteritems(self.changes_dict):
for field_name, values in self.changes_dict.items():
# try to get the field attribute on the model
try:
field = model._meta.get_field(field_name)
@ -259,9 +255,9 @@ class LogEntry(models.Model):
values_display = []
# handle choices fields and Postgres ArrayField to get human readable version
choices_dict = None
if hasattr(field, 'choices') and len(field.choices) > 0:
if getattr(field, 'choices', []):
choices_dict = dict(field.choices)
if hasattr(field, 'base_field') and getattr(field.base_field, 'choices', False):
if getattr(getattr(field, 'base_field', None), 'choices', []):
choices_dict = dict(field.base_field.choices)
if choices_dict:
@ -355,6 +351,7 @@ class AuditlogHistoryField(GenericRelation):
# South compatibility for AuditlogHistoryField
try:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"])
raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.")
except ImportError:

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import json
from auditlog.diff import model_instance_diff

View file

@ -1,15 +1,19 @@
from __future__ import unicode_literals
from typing import Dict, Callable, Optional, List, Tuple
from django.db.models.signals import pre_save, post_save, post_delete
from django.db.models import Model
from django.utils.six import iteritems
from django.db.models.base import ModelBase
from django.db.models.signals import pre_save, post_save, post_delete, ModelSignal
DispatchUID = Tuple[int, str, int]
class AuditlogModelRegistry(object):
"""
A registry that keeps track of the models that use Auditlog to track changes.
"""
def __init__(self, create=True, update=True, delete=True, custom=None):
def __init__(self, create: bool = True, update: bool = True, delete: bool = True,
custom: Optional[Dict[ModelSignal, Callable]] = None):
from auditlog.receivers import log_create, log_update, log_delete
self._registry = {}
@ -25,17 +29,25 @@ class AuditlogModelRegistry(object):
if custom is not None:
self._signals.update(custom)
def register(self, model=None, include_fields=[], exclude_fields=[], mapping_fields={}):
def register(self, model: ModelBase = None, include_fields: Optional[List[str]] = None,
exclude_fields: Optional[List[str]] = None, mapping_fields: Optional[Dict[str, str]] = None):
"""
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
:param model: The model to register.
:type model: Model
:param include_fields: The fields to include. Implicitly excludes all other fields.
:type include_fields: list
:param exclude_fields: The fields to exclude. Overrides the fields to include.
:type exclude_fields: list
:param mapping_fields: Mapping from field names to strings in diff.
"""
if include_fields is None:
include_fields = []
if exclude_fields is None:
exclude_fields = []
if mapping_fields is None:
mapping_fields = {}
def registrar(cls):
"""Register models for a given class."""
if not issubclass(cls, Model):
@ -61,23 +73,21 @@ class AuditlogModelRegistry(object):
# Otherwise, just register the model.
registrar(model)
def contains(self, model):
def contains(self, model: ModelBase) -> bool:
"""
Check if a model is registered with auditlog.
:param model: The model to check.
:type model: Model
:return: Whether the model has been registered.
:rtype: bool
"""
return model in self._registry
def unregister(self, model):
def unregister(self, model: ModelBase) -> None:
"""
Unregister a model with auditlog. This will not affect the database.
:param model: The model to unregister.
:type model: Model
"""
try:
del self._registry[model]
@ -86,6 +96,19 @@ class AuditlogModelRegistry(object):
else:
self._disconnect_signals(model)
def get_models(self) -> List[ModelBase]:
"""Get a list of all registered models."""
return list(self._registry.keys())
list = get_models # TODO: remove after all users migrate to get_models
def get_model_fields(self, model: ModelBase):
return {
'include_fields': list(self._registry[model]['include_fields']),
'exclude_fields': list(self._registry[model]['exclude_fields']),
'mapping_fields': dict(self._registry[model]['mapping_fields']),
}
def _connect_signals(self, model):
"""
Connect signals for the model.
@ -101,24 +124,11 @@ class AuditlogModelRegistry(object):
for signal, receiver in self._signals.items():
signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model))
def _dispatch_uid(self, signal, model):
def _dispatch_uid(self, signal, model) -> DispatchUID:
"""
Generate a dispatch_uid.
"""
return (self.__class__, model, signal)
def get_model_fields(self, model):
return {
'include_fields': self._registry[model]['include_fields'],
'exclude_fields': self._registry[model]['exclude_fields'],
'mapping_fields': self._registry[model]['mapping_fields'],
}
class AuditLogModelRegistry(AuditlogModelRegistry):
def __init__(self, *args, **kwargs):
super(AuditLogModelRegistry, self).__init__(*args, **kwargs)
raise DeprecationWarning("Use AuditlogModelRegistry instead of AuditLogModelRegistry, AuditLogModelRegistry will be removed in django-auditlog 0.4.0 or later.")
return self.__hash__(), model.__qualname__, signal.__hash__()
auditlog = AuditlogModelRegistry()

View file

@ -2,7 +2,6 @@
Settings file for the Auditlog test suite.
"""
import os
import django
SECRET_KEY = 'test'
@ -17,32 +16,22 @@ INSTALLED_APPS = [
'multiselectfield',
]
middlewares = (
MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'auditlog.middleware.AuditlogMiddleware',
)
if django.VERSION < (1, 10):
MIDDLEWARE_CLASSES = middlewares
else:
MIDDLEWARE = middlewares
if django.VERSION <= (1, 9):
POSTGRES_DRIVER = 'django.db.backends.postgresql_psycopg2'
else:
POSTGRES_DRIVER = 'django.db.backends.postgresql'
]
DATABASES = {
'default': {
'ENGINE': POSTGRES_DRIVER,
'NAME': 'auditlog_tests_db',
'USER': 'postgres',
'PASSWORD': '',
'HOST': '127.0.0.1',
'PORT': '5432',
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('TEST_DB_NAME', 'auditlog' + os.environ.get("TOX_PARALLEL_ENV", "")),
'USER': os.getenv('TEST_DB_USER', 'postgres'),
'PASSWORD': os.getenv('TEST_DB_PASS', ''),
'HOST': os.getenv('TEST_DB_HOST', '127.0.0.1'),
'PORT': os.getenv('TEST_DB_PORT', '5432'),
}
}

View file

@ -1,14 +1,11 @@
import datetime
import django
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import ValidationError
from django.db.models.signals import pre_save
from django.http import HttpResponse
from django.test import TestCase, RequestFactory
from django.utils import dateformat, formats, timezone
from dateutil.tz import gettz
import mock
from auditlog.middleware import AuditlogMiddleware
from auditlog.models import LogEntry
@ -17,7 +14,6 @@ from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKe
ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \
ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \
CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel
from auditlog import compat
class SimpleModelTest(TestCase):
@ -121,66 +117,64 @@ class MiddlewareTest(TestCase):
Test the middleware responsible for connecting and disconnecting the signals used in automatic logging.
"""
def setUp(self):
self.middleware = AuditlogMiddleware()
self.get_response_mock = mock.Mock()
self.response_mock = mock.Mock()
self.middleware = AuditlogMiddleware(get_response=self.get_response_mock)
self.factory = RequestFactory()
self.user = User.objects.create_user(username='test', email='test@example.com', password='top_secret')
def side_effect(self, assertion):
def inner(request):
assertion()
return self.response_mock
return inner
def assert_has_listeners(self):
self.assertTrue(pre_save.has_listeners(LogEntry))
def assert_no_listeners(self):
self.assertFalse(pre_save.has_listeners(LogEntry))
def test_request_anonymous(self):
"""No actor will be logged when a user is not logged in."""
# Create a request
request = self.factory.get('/')
request.user = AnonymousUser()
# Run middleware
self.middleware.process_request(request)
self.get_response_mock.side_effect = self.side_effect(self.assert_no_listeners)
# Validate result
self.assertFalse(pre_save.has_listeners(LogEntry))
response = self.middleware(request)
# Finalize transaction
self.middleware.process_exception(request, None)
self.assertIs(response, self.response_mock)
self.get_response_mock.assert_called_once_with(request)
self.assert_no_listeners()
def test_request(self):
"""The actor will be logged when a user is logged in."""
# Create a request
request = self.factory.get('/')
request.user = self.user
# Run middleware
self.middleware.process_request(request)
# Validate result
self.assertTrue(pre_save.has_listeners(LogEntry))
# Finalize transaction
self.middleware.process_exception(request, None)
def test_response(self):
"""The signal will be disconnected when the request is processed."""
# Create a request
request = self.factory.get('/')
request.user = self.user
# Run middleware
self.middleware.process_request(request)
self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it.
self.middleware.process_response(request, HttpResponse())
self.get_response_mock.side_effect = self.side_effect(self.assert_has_listeners)
# Validate result
self.assertFalse(pre_save.has_listeners(LogEntry))
response = self.middleware(request)
self.assertIs(response, self.response_mock)
self.get_response_mock.assert_called_once_with(request)
self.assert_no_listeners()
def test_exception(self):
"""The signal will be disconnected when an exception is raised."""
# Create a request
request = self.factory.get('/')
request.user = self.user
# Run middleware
self.middleware.process_request(request)
self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it.
self.middleware.process_exception(request, ValidationError("Test"))
SomeException = type('SomeException', (Exception,), {})
# Validate result
self.assertFalse(pre_save.has_listeners(LogEntry))
self.get_response_mock.side_effect = SomeException
with self.assertRaises(SomeException):
self.middleware(request)
self.assert_no_listeners()
class SimpeIncludeModelTest(TestCase):
@ -603,44 +597,6 @@ class PostgresArrayFieldModelTest(TestCase):
msg="The human readable text 'Green' is displayed.")
class CompatibilityTest(TestCase):
"""Test case for compatibility functions."""
def test_is_authenticated(self):
"""Test that the 'is_authenticated' compatibility function is working.
Bit of explanation: the `is_authenticated` property on request.user is
*always* set to 'False' for AnonymousUser, and it is *always* set to
'True' for *any* other (i.e. identified/authenticated) user.
So, the logic of this test is to ensure that compat.is_authenticated()
returns the correct value based on whether or not the User is an
anonymous user (simulating what goes on in the real request.user).
"""
# Test compat.is_authenticated for anonymous users
self.user = auth.get_user(self.client)
if django.VERSION < (1, 10):
assert self.user.is_anonymous()
else:
assert self.user.is_anonymous
assert not compat.is_authenticated(self.user)
# Setup some other user, which is *not* anonymous, and check
# compat.is_authenticated
self.user = User.objects.create(
username="test.user",
email="test.user@mail.com",
password="auditlog"
)
if django.VERSION < (1, 10):
assert not self.user.is_anonymous()
else:
assert not self.user.is_anonymous
assert compat.is_authenticated(self.user)
class AdminPanelTest(TestCase):
@classmethod
def setUpTestData(cls):

7
auditlog_tests/urls.py Normal file
View file

@ -0,0 +1,7 @@
from django.conf.urls import url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
]

View file

@ -1,177 +1,20 @@
# Makefile for Sphinx documentation
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
clean:
rm -rf $(BUILDDIR)/*
.PHONY: help Makefile
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-auditlog.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-auditlog.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-auditlog"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-auditlog"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View file

@ -1,242 +1,35 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
set I18NSPHINXOPTS=%SPHINXOPTS% source
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-auditlog.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-auditlog.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View file

@ -1,35 +1,40 @@
# -*- coding: utf-8 -*-
# Configuration file for the Sphinx documentation builder.
#
# django-auditlog documentation build configuration file, created by
# sphinx-quickstart on Wed Nov 6 20:39:48 2013.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Preliminary -------------------------------------------------------------
import sys
import os
import sphinx_rtd_theme
import sys
from datetime import date
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('../../src'))
# Django settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproject.settings')
from django.conf import settings
settings.configure()
# Add sources folder
sys.path.insert(0, os.path.abspath('../../'))
# -- General configuration ------------------------------------------------
# Setup Django for autodoc
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'auditlog_tests.test_settings')
import django
django.setup()
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# -- Project information -----------------------------------------------------
project = 'django-auditlog'
author = 'Jan-Jelle Kester and contributors'
copyright = f'2013-{date.today().year}, {author}'
# The full version, including alpha/beta/rc tags
import auditlog
release = auditlog.__version__
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -39,229 +44,25 @@ extensions = [
'sphinx.ext.viewcode',
]
# Master document that contains the root table of contents
master_doc = 'index'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'django-auditlog'
copyright = u'2017, Jan-Jelle Kester and contributors'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.4'
# The full version, including alpha/beta/rc tags.
release = '0.4.7'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-auditlogdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'django-auditlog.tex', u'django-auditlog Documentation',
u'Jan-Jelle Kester', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'django-auditlog', u'django-auditlog Documentation',
[u'Jan-Jelle Kester'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'django-auditlog', u'django-auditlog Documentation',
u'Jan-Jelle Kester', 'django-auditlog', '',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

View file

@ -36,5 +36,5 @@ Contribute to Auditlog
If you discovered a bug or want to improve the code, please submit an issue and/or pull request via GitHub.
Before submitting a new issue, please make sure there is no issue submitted that involves the same problem.
| GitHub repository: https://github.com/jjkester/django-auditlog
| Issues: https://github.com/jjkester/django-auditlog/issues
| GitHub repository: https://github.com/jazzband/django-auditlog
| Issues: https://github.com/jazzband/django-auditlog/issues

View file

@ -7,15 +7,15 @@ way to do this is by using the Python Package Index (PyPI). Simply run the follo
``pip install django-auditlog``
Instead of installing Auditlog via PyPI, you can also clone the Git repository or download the source code via GitHub.
The repository can be found at https://github.com/jjkester/django-auditlog/.
The repository can be found at https://github.com/jazzband/django-auditlog/.
**Requirements**
- Python 2.7, 3.4 or higher
- Django 1.8 or higher
- Python 3.5 or higher
- Django 2.2 or higher
Auditlog is currently tested with Python 2.7 and 3.4 and Django 1.8, 1.9 and 1.10. The latest test report can be found
at https://travis-ci.org/jjkester/django-auditlog.
Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2 and 3.0. The latest test report can be found
at https://travis-ci.org/jazzband/django-auditlog.
Adding Auditlog to your Django application
------------------------------------------

View file

@ -191,8 +191,8 @@ Management commands
Auditlog provides the ``auditlogflush`` management command to clear all log entries from the database.
The command asks for confirmation, it is not possible to execute the command without giving any form of (simulated) user
input.
By default, the command asks for confirmation. It is possible to run the command with the `-y` or `--yes` flag to skip
confirmation and immediately delete all entries.
.. warning::

View file

@ -1,7 +0,0 @@
-r requirements.txt
coverage==4.3.4
tox>=1.7.0
codecov>=2.0.0
django-multiselectfield==0.1.8
psycopg2-binary

View file

@ -1,2 +1,15 @@
django-jsonfield>=1.0.0
python-dateutil==2.6.0
# Build requirements
setuptools
wheel
# Docs requirements
sphinx
sphinx_rtd_theme
# Test requirements
coverage
tox
codecov
django-multiselectfield
mock
psycopg2-binary

View file

@ -1,27 +1,36 @@
from distutils.core import setup
import os
from setuptools import setup
import auditlog
# Readme as long description
with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as readme_file:
long_description = readme_file.read()
setup(
name='django-auditlog',
version='0.4.7',
version=auditlog.__version__,
packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'],
package_dir={'': 'src'},
url='https://github.com/jjkester/django-auditlog',
url='https://github.com/MacmillanPlatform/django-auditlog/',
license='MIT',
author='Jan-Jelle Kester',
maintainer='Alieh Rymašeŭski',
description='Audit log app for Django',
long_description=long_description,
long_description_content_type='text/markdown',
install_requires=[
'django-admin-rangefilter>=0.5.0',
'django-jsonfield>=1.0.0',
'python-dateutil==2.6.0'
'python-dateutil>=2.6.0',
],
zip_safe=False,
classifiers=[
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'License :: OSI Approved :: MIT License',
],
],
)

View file

@ -1,18 +0,0 @@
from django.contrib import admin
from .models import LogEntry
from .mixins import LogEntryAdminMixin
from .filters import ResourceTypeFilter
class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin):
list_display = ['created', 'resource_url', 'action', 'msg_short', 'user_url']
search_fields = ['timestamp', 'object_repr', 'changes', 'actor__first_name', 'actor__last_name']
list_filter = ['action', ResourceTypeFilter]
readonly_fields = ['created', 'resource_url', 'action', 'user_url', 'msg']
fieldsets = [
(None, {'fields': ['created', 'user_url', 'resource_url']}),
('Changes', {'fields': ['action', 'msg']}),
]
admin.site.register(LogEntry, LogEntryAdmin)

View file

@ -1,20 +0,0 @@
import django
def is_authenticated(user):
"""Return whether or not a User is authenticated.
Function provides compatibility following deprecation of method call to
`is_authenticated()` in Django 2.0.
This is *only* required to support Django < v1.10 (i.e. v1.9 and earlier),
as `is_authenticated` was introduced as a property in v1.10.s
"""
if not hasattr(user, 'is_authenticated'):
return False
if callable(user.is_authenticated):
# Will be callable if django.version < 2.0, but is only necessary in
# v1.9 and earlier due to change introduced in v1.10 making
# `is_authenticated` a property instead of a callable.
return user.is_authenticated()
else:
return user.is_authenticated

View file

@ -1,16 +0,0 @@
from django.contrib.admin import SimpleListFilter
class ResourceTypeFilter(SimpleListFilter):
title = 'Resource Type'
parameter_name = 'resource_type'
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request)
types = qs.values_list('content_type_id', 'content_type__model')
return list(types.order_by('content_type__model').distinct())
def queryset(self, request, queryset):
if self.value() is None:
return queryset
return queryset.filter(content_type_id=self.value())

View file

@ -1,20 +0,0 @@
from django.core.management.base import BaseCommand
from six import moves
from auditlog.models import LogEntry
class Command(BaseCommand):
help = 'Deletes all log entries from the database.'
def handle(self, *args, **options):
answer = None
while answer not in ['', 'y', 'n']:
answer = moves.input("Are you sure? [y/N]: ").lower().strip()
if answer == 'y':
count = LogEntry.objects.all().count()
LogEntry.objects.all().delete()
print("Deleted %d objects." % count)

View file

@ -1,84 +0,0 @@
from __future__ import unicode_literals
import threading
import time
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils.functional import curry
from django.apps import apps
from auditlog.models import LogEntry
from auditlog.compat import is_authenticated
# Use MiddlewareMixin when present (Django >= 1.10)
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object
threadlocal = threading.local()
class AuditlogMiddleware(MiddlewareMixin):
"""
Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the
user from the request (or None if the user is not authenticated).
"""
def process_request(self, request):
"""
Gets the current user from the request and prepares and connects a signal receiver with the user already
attached to it.
"""
# Initialize thread local storage
threadlocal.auditlog = {
'signal_duid': (self.__class__, time.time()),
'remote_addr': request.META.get('REMOTE_ADDR'),
}
# In case of proxy, set 'original' address
if request.META.get('HTTP_X_FORWARDED_FOR'):
threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0]
# Connect signal for automatic logging
if hasattr(request, 'user') and is_authenticated(request.user):
set_actor = curry(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid'])
pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False)
def process_response(self, request, response):
"""
Disconnects the signal receiver to prevent it from staying active.
"""
if hasattr(threadlocal, 'auditlog'):
pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
return response
def process_exception(self, request, exception):
"""
Disconnects the signal receiver to prevent it from staying active in case of an exception.
"""
if hasattr(threadlocal, 'auditlog'):
pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
return None
@staticmethod
def set_actor(user, sender, instance, signal_duid, **kwargs):
"""
Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when
it is curried with the actor.
"""
if hasattr(threadlocal, 'auditlog'):
if signal_duid != threadlocal.auditlog['signal_duid']:
return
try:
app_label, model_name = settings.AUTH_USER_MODEL.split('.')
auth_user_model = apps.get_model(app_label, model_name)
except ValueError:
auth_user_model = apps.get_model('auth', 'user')
if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None:
instance.actor = user
instance.remote_addr = threadlocal.auditlog['remote_addr']

View file

@ -1,13 +0,0 @@
import django
from django.conf.urls import include, url
from django.contrib import admin
if django.VERSION < (1, 9):
admin_urls = include(admin.site.urls)
else:
admin_urls = admin.site.urls
urlpatterns = [
url(r'^admin/', admin_urls),
]

19
tox.ini
View file

@ -1,23 +1,18 @@
[tox]
envlist =
{py27,py34,py35,py36,py37}-django-111
{py34,py35,py36,py37}-django-20
{py35,py36,py37}-django-21
{py35,py36,py37}-django-22
{py35,py36,py37,py38}-django-22
{py36,py37,py38}-django-30
[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/src/auditlog
commands = coverage run --source src/auditlog src/runtests.py
PYTHONPATH = {toxinidir}:{toxinidir}/auditlog
commands = coverage run -p --source auditlog runtests.py
deps =
django-111: Django>=1.11,<2.0
django-20: Django>=2.0,<2.1
django-21: Django>=2.1,<2.2
django-22: Django>=2.2,<2.3
-r{toxinidir}/requirements-test.txt
django-30: Django>=3.0,<3.1
-r{toxinidir}/requirements.txt
basepython =
py38: python3.8
py37: python3.7
py36: python3.6
py35: python3.5
py34: python3.4
py27: python2.7