mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-04-23 00:04:47 +00:00
Compare commits
64 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a0726982 | ||
|
|
ff6349a89a | ||
|
|
8b97cc2acb | ||
|
|
5dcb069bb8 | ||
|
|
d7a6496ad8 | ||
|
|
df16b2a8da | ||
|
|
be82018266 | ||
|
|
de693dd092 | ||
|
|
6379683f77 | ||
|
|
c346adb8b5 | ||
|
|
701f867a04 | ||
|
|
0ca00faafc | ||
|
|
cb1fefb793 | ||
|
|
536a841bf3 | ||
|
|
08a7b82acc | ||
|
|
8d2bb0f319 | ||
|
|
3ac4311ae4 | ||
|
|
917b490ee4 | ||
|
|
47a268eef9 | ||
|
|
ca29848d78 | ||
|
|
a6cea38c9e | ||
|
|
33a35e841b | ||
|
|
a11d1a3e46 | ||
|
|
45a27ec1d6 | ||
|
|
e6f3f12bae | ||
|
|
28a60af5f3 | ||
|
|
fd169771df | ||
|
|
d97ac056d4 | ||
|
|
33c35b42db | ||
|
|
0f3c7b430f | ||
|
|
bf8ba7a0be | ||
|
|
2b536fc87e | ||
|
|
106417c684 | ||
|
|
358971aafe | ||
|
|
f58b3d7685 | ||
|
|
5cd55ac38c | ||
|
|
8397754a20 | ||
|
|
da49432924 | ||
|
|
3af06e13c7 | ||
|
|
ae57b0c322 | ||
|
|
bde49bdb4f | ||
|
|
c66b36c700 | ||
|
|
2ae401a04f | ||
|
|
5ba554af56 | ||
|
|
df2bf0a05c | ||
|
|
05e6b179fd | ||
|
|
07b38a9345 | ||
|
|
5784247180 | ||
|
|
2a43cff96f | ||
|
|
b16b1a0df3 | ||
|
|
9152d225bb | ||
|
|
c53b766132 | ||
|
|
e35d0f4194 | ||
|
|
e60876ae14 | ||
|
|
5dbea8a9a1 | ||
|
|
cfbc588cc1 | ||
|
|
ee6bb33bc9 | ||
|
|
c9c97b6861 | ||
|
|
5f5cc7f7e9 | ||
|
|
a5381b6195 | ||
|
|
2dc0ac43b5 | ||
|
|
aa28009d3b | ||
|
|
03b8616dac | ||
|
|
62c1e676cc |
55 changed files with 670 additions and 1126 deletions
90
.gitignore
vendored
90
.gitignore
vendored
|
|
@ -1,12 +1,82 @@
|
||||||
*.db
|
### Python template
|
||||||
*.egg-info
|
# Byte-compiled / optimized / DLL files
|
||||||
*.log
|
__pycache__/
|
||||||
*.pot
|
*.py[cod]
|
||||||
*.pyc
|
*$py.class
|
||||||
.idea
|
|
||||||
.project
|
# C extensions
|
||||||
.pydevproject
|
*.so
|
||||||
.coverage
|
|
||||||
venv/
|
# 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/
|
.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
|
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/
|
||||||
|
|
|
||||||
43
.travis.yml
43
.travis.yml
|
|
@ -11,44 +11,29 @@ addons:
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
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
|
- python: 3.5
|
||||||
env: TOXENV=py35-django-22
|
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
|
- python: 3.6
|
||||||
env: TOXENV=py36-django-22
|
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
|
- python: 3.7
|
||||||
env: TOXENV=py37-django-22
|
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
|
fast_finish: true
|
||||||
|
|
||||||
install: pip install -r requirements-test.txt
|
install: pip install -r requirements.txt
|
||||||
|
|
||||||
script: tox
|
script: tox
|
||||||
|
|
||||||
|
|
@ -59,7 +44,7 @@ deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
# PyPI credentials supplied with environment variables from repository settings
|
# PyPI credentials supplied with environment variables from repository settings
|
||||||
on:
|
on:
|
||||||
repo: jjkester/django-auditlog
|
repo: jazzband/django-auditlog
|
||||||
branch: stable
|
branch: stable
|
||||||
condition: $TOXENV = py36-django-20
|
condition: $TOXENV = py38-django-30
|
||||||
edge: true
|
edge: true
|
||||||
|
|
|
||||||
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[](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).
|
||||||
3
LICENSE
3
LICENSE
|
|
@ -1,6 +1,7 @@
|
||||||
The MIT License (MIT)
|
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
|
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
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
||||||
24
MANIFEST
24
MANIFEST
|
|
@ -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
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
django-auditlog
|
django-auditlog
|
||||||
===============
|
===============
|
||||||
|
|
||||||
[](https://travis-ci.org/jjkester/django-auditlog)
|
[](https://jazzband.co/)
|
||||||
|
[](https://travis-ci.org/jazzband/django-auditlog)
|
||||||
[](http://django-auditlog.readthedocs.org/en/latest/?badge=latest)
|
[](http://django-auditlog.readthedocs.org/en/latest/?badge=latest)
|
||||||
|
|
||||||
**Please remember that this app is still in development.**
|
**Please remember that this app is still in development.**
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
from __future__ import unicode_literals
|
__version__ = '0.7.2'
|
||||||
|
|
||||||
default_app_config = 'auditlog.apps.AuditlogConfig'
|
default_app_config = 'auditlog.apps.AuditlogConfig'
|
||||||
34
auditlog/admin.py
Normal file
34
auditlog/admin.py
Normal 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)
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
55
auditlog/context.py
Normal file
55
auditlog/context.py
Normal 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
23
auditlog/count.py
Normal 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
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Model, NOT_PROVIDED, DateTimeField
|
from django.db.models import Model, NOT_PROVIDED, DateTimeField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_str
|
||||||
|
|
||||||
|
|
||||||
def track_field(field):
|
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:
|
if getattr(field, 'remote_field', None) is not None and field.remote_field.model == LogEntry:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 1.8 check
|
|
||||||
elif getattr(field, 'rel', None) is not None and field.rel.to == LogEntry:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -46,17 +40,13 @@ def get_fields_in_model(instance):
|
||||||
"""
|
"""
|
||||||
assert isinstance(instance, Model)
|
assert isinstance(instance, Model)
|
||||||
|
|
||||||
# Check if the Django 1.8 _meta API is available
|
return [f for f in instance._meta.get_fields() if track_field(f)]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def get_field_value(obj, field):
|
def get_field_value(obj, field):
|
||||||
"""
|
"""
|
||||||
Gets the value of a given model instance field.
|
Gets the value of a given model instance field.
|
||||||
|
|
||||||
:param obj: The model instance.
|
:param obj: The model instance.
|
||||||
:type obj: Model
|
:type obj: Model
|
||||||
:param field: The field you want to find the value of.
|
: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):
|
if isinstance(field, DateTimeField):
|
||||||
# DateTimeFields are timezone-aware, so we need to convert the field
|
# 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:
|
try:
|
||||||
value = field.to_python(getattr(obj, field.name, None))
|
value = field.to_python(getattr(obj, field.name, None))
|
||||||
if value is not None and settings.USE_TZ and not timezone.is_naive(value):
|
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
|
value = field.default if field.default is not NOT_PROVIDED else None
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
value = smart_text(getattr(obj, field.name, None))
|
value = smart_str(getattr(obj, field.name, None))
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
value = field.default if field.default is not NOT_PROVIDED else None
|
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
|
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.")
|
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.")
|
raise TypeError("The supplied new instance is not a valid model instance.")
|
||||||
|
|
||||||
diff = {}
|
diff = {}
|
||||||
|
|
@ -135,7 +125,7 @@ def model_instance_diff(old, new):
|
||||||
new_value = get_field_value(new, field)
|
new_value = get_field_value(new, field)
|
||||||
|
|
||||||
if old_value != new_value:
|
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:
|
if len(diff) == 0:
|
||||||
diff = None
|
diff = None
|
||||||
96
auditlog/filters.py
Normal file
96
auditlog/filters.py
Normal 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
|
||||||
25
auditlog/management/commands/auditlogflush.py
Normal file
25
auditlog/management/commands/auditlogflush.py
Normal 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
35
auditlog/middleware.py
Normal 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)
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
from django.db import migrations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
import jsonfield.fields
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
from django.db import migrations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import jsonfield.fields
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
17
auditlog/migrations/0008_timestamp_index.py
Normal file
17
auditlog/migrations/0008_timestamp_index.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
21
auditlog/migrations/0009_timestamp_id_index.py
Normal file
21
auditlog/migrations/0009_timestamp_id_index.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django import urls as urlresolvers
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
try:
|
from django.urls.exceptions import NoReverseMatch
|
||||||
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.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.timezone import localtime
|
||||||
|
|
||||||
|
from auditlog.models import LogEntry
|
||||||
|
|
||||||
MAX = 75
|
MAX = 75
|
||||||
|
|
||||||
|
|
@ -18,7 +15,8 @@ MAX = 75
|
||||||
class LogEntryAdminMixin(object):
|
class LogEntryAdminMixin(object):
|
||||||
|
|
||||||
def created(self, obj):
|
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'
|
created.short_description = 'Created'
|
||||||
|
|
||||||
def user_url(self, obj):
|
def user_url(self, obj):
|
||||||
|
|
@ -26,12 +24,13 @@ class LogEntryAdminMixin(object):
|
||||||
app_label, model = settings.AUTH_USER_MODEL.split('.')
|
app_label, model = settings.AUTH_USER_MODEL.split('.')
|
||||||
viewname = 'admin:%s_%s_change' % (app_label, model.lower())
|
viewname = 'admin:%s_%s_change' % (app_label, model.lower())
|
||||||
try:
|
try:
|
||||||
link = urlresolvers.reverse(viewname, args=[obj.actor.id])
|
link = urlresolvers.reverse(viewname, args=[obj.actor.pk])
|
||||||
except NoReverseMatch:
|
except NoReverseMatch:
|
||||||
return u'%s' % (obj.actor)
|
return u'%s' % (obj.actor)
|
||||||
return format_html(u'<a href="{}">{}</a>', link, obj.actor)
|
return format_html(u'<a href="{}">{}</a>', link, obj.actor)
|
||||||
|
|
||||||
return 'system'
|
return 'system'
|
||||||
|
|
||||||
user_url.short_description = 'User'
|
user_url.short_description = 'User'
|
||||||
|
|
||||||
def resource_url(self, obj):
|
def resource_url(self, obj):
|
||||||
|
|
@ -44,10 +43,11 @@ class LogEntryAdminMixin(object):
|
||||||
return obj.object_repr
|
return obj.object_repr
|
||||||
else:
|
else:
|
||||||
return format_html(u'<a href="{}">{}</a>', link, obj.object_repr)
|
return format_html(u'<a href="{}">{}</a>', link, obj.object_repr)
|
||||||
|
|
||||||
resource_url.short_description = 'Resource'
|
resource_url.short_description = 'Resource'
|
||||||
|
|
||||||
def msg_short(self, obj):
|
def msg_short(self, obj):
|
||||||
if obj.action == 2:
|
if obj.action == LogEntry.Action.DELETE:
|
||||||
return '' # delete
|
return '' # delete
|
||||||
changes = json.loads(obj.changes)
|
changes = json.loads(obj.changes)
|
||||||
s = '' if len(changes) == 1 else 's'
|
s = '' if len(changes) == 1 else 's'
|
||||||
|
|
@ -56,10 +56,11 @@ class LogEntryAdminMixin(object):
|
||||||
i = fields.rfind(' ', 0, MAX)
|
i = fields.rfind(' ', 0, MAX)
|
||||||
fields = fields[:i] + ' ..'
|
fields = fields[:i] + ' ..'
|
||||||
return '%d change%s: %s' % (len(changes), s, fields)
|
return '%d change%s: %s' % (len(changes), s, fields)
|
||||||
|
|
||||||
msg_short.short_description = 'Changes'
|
msg_short.short_description = 'Changes'
|
||||||
|
|
||||||
def msg(self, obj):
|
def msg(self, obj):
|
||||||
if obj.action == 2:
|
if obj.action == LogEntry.Action.DELETE:
|
||||||
return '' # delete
|
return '' # delete
|
||||||
changes = json.loads(obj.changes)
|
changes = json.loads(obj.changes)
|
||||||
msg = '<table><tr><th>#</th><th>Field</th><th>From</th><th>To</th></tr>'
|
msg = '<table><tr><th>#</th><th>Field</th><th>From</th><th>To</th></tr>'
|
||||||
|
|
@ -69,4 +70,5 @@ class LogEntryAdminMixin(object):
|
||||||
|
|
||||||
msg += '</table>'
|
msg += '</table>'
|
||||||
return mark_safe(msg)
|
return mark_safe(msg)
|
||||||
|
|
||||||
msg.short_description = 'Changes'
|
msg.short_description = 'Changes'
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
import ast
|
import ast
|
||||||
|
import json
|
||||||
|
|
||||||
|
from dateutil import parser
|
||||||
|
from dateutil.tz import gettz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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 import models, DEFAULT_DB_ALIAS
|
||||||
from django.db.models import QuerySet, Q
|
from django.db.models import QuerySet, Q
|
||||||
from django.utils import formats, timezone
|
from django.utils import formats, timezone
|
||||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
from django.utils.encoding import smart_str
|
||||||
from django.utils.six import iteritems, integer_types
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from jsonfield.fields import JSONField
|
from jsonfield.fields import JSONField
|
||||||
from dateutil import parser
|
|
||||||
from dateutil.tz import gettz
|
|
||||||
|
|
||||||
|
|
||||||
class LogEntryManager(models.Manager):
|
class LogEntryManager(models.Manager):
|
||||||
|
|
@ -41,9 +37,9 @@ class LogEntryManager(models.Manager):
|
||||||
if changes is not None:
|
if changes is not None:
|
||||||
kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance))
|
kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance))
|
||||||
kwargs.setdefault('object_pk', pk)
|
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)
|
kwargs.setdefault('object_id', pk)
|
||||||
|
|
||||||
get_additional_data = getattr(instance, 'get_additional_data', None)
|
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__)
|
content_type = ContentType.objects.get_for_model(instance.__class__)
|
||||||
pk = self._get_pk_value(instance)
|
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)
|
return self.filter(content_type=content_type, object_id=pk)
|
||||||
else:
|
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):
|
def get_for_objects(self, queryset):
|
||||||
"""
|
"""
|
||||||
|
|
@ -98,10 +94,10 @@ class LogEntryManager(models.Manager):
|
||||||
content_type = ContentType.objects.get_for_model(queryset.model)
|
content_type = ContentType.objects.get_for_model(queryset.model)
|
||||||
primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True))
|
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()
|
return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct()
|
||||||
elif isinstance(queryset.model._meta.pk, models.UUIDField):
|
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()
|
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
|
||||||
else:
|
else:
|
||||||
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
|
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
|
return pk
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class LogEntry(models.Model):
|
class LogEntry(models.Model):
|
||||||
"""
|
"""
|
||||||
Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available)
|
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']
|
ordering = ['-timestamp']
|
||||||
verbose_name = _("log entry")
|
verbose_name = _("log entry")
|
||||||
verbose_name_plural = _("log entries")
|
verbose_name_plural = _("log entries")
|
||||||
|
index_together = (("timestamp", "id"))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.action == self.Action.CREATE:
|
if self.action == self.Action.CREATE:
|
||||||
|
|
@ -213,7 +209,7 @@ class LogEntry(models.Model):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@property
|
@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
|
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
|
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 = []
|
substrings = []
|
||||||
|
|
||||||
for field, values in iteritems(self.changes_dict):
|
for field, values in self.changes_dict.items():
|
||||||
substring = smart_text('{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format(
|
substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format(
|
||||||
field_name=field,
|
field_name=field,
|
||||||
colon=colon,
|
colon=colon,
|
||||||
old=values[0],
|
old=values[0],
|
||||||
|
|
@ -249,7 +245,7 @@ class LogEntry(models.Model):
|
||||||
model_fields = auditlog.get_model_fields(model._meta.model)
|
model_fields = auditlog.get_model_fields(model._meta.model)
|
||||||
changes_display_dict = {}
|
changes_display_dict = {}
|
||||||
# grab the changes_dict and iterate through
|
# 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 to get the field attribute on the model
|
||||||
try:
|
try:
|
||||||
field = model._meta.get_field(field_name)
|
field = model._meta.get_field(field_name)
|
||||||
|
|
@ -259,9 +255,9 @@ class LogEntry(models.Model):
|
||||||
values_display = []
|
values_display = []
|
||||||
# handle choices fields and Postgres ArrayField to get human readable version
|
# handle choices fields and Postgres ArrayField to get human readable version
|
||||||
choices_dict = None
|
choices_dict = None
|
||||||
if hasattr(field, 'choices') and len(field.choices) > 0:
|
if getattr(field, 'choices', []):
|
||||||
choices_dict = dict(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)
|
choices_dict = dict(field.base_field.choices)
|
||||||
|
|
||||||
if choices_dict:
|
if choices_dict:
|
||||||
|
|
@ -355,6 +351,7 @@ class AuditlogHistoryField(GenericRelation):
|
||||||
# South compatibility for AuditlogHistoryField
|
# South compatibility for AuditlogHistoryField
|
||||||
try:
|
try:
|
||||||
from south.modelsinspector import add_introspection_rules
|
from south.modelsinspector import add_introspection_rules
|
||||||
|
|
||||||
add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"])
|
add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"])
|
||||||
raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.")
|
raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from auditlog.diff import model_instance_diff
|
from auditlog.diff import model_instance_diff
|
||||||
|
|
@ -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.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):
|
class AuditlogModelRegistry(object):
|
||||||
"""
|
"""
|
||||||
A registry that keeps track of the models that use Auditlog to track changes.
|
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
|
from auditlog.receivers import log_create, log_update, log_delete
|
||||||
|
|
||||||
self._registry = {}
|
self._registry = {}
|
||||||
|
|
@ -25,17 +29,25 @@ class AuditlogModelRegistry(object):
|
||||||
if custom is not None:
|
if custom is not None:
|
||||||
self._signals.update(custom)
|
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.
|
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
|
||||||
|
|
||||||
:param model: The model to register.
|
:param model: The model to register.
|
||||||
:type model: Model
|
|
||||||
:param include_fields: The fields to include. Implicitly excludes all other fields.
|
: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.
|
: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):
|
def registrar(cls):
|
||||||
"""Register models for a given class."""
|
"""Register models for a given class."""
|
||||||
if not issubclass(cls, Model):
|
if not issubclass(cls, Model):
|
||||||
|
|
@ -61,23 +73,21 @@ class AuditlogModelRegistry(object):
|
||||||
# Otherwise, just register the model.
|
# Otherwise, just register the model.
|
||||||
registrar(model)
|
registrar(model)
|
||||||
|
|
||||||
def contains(self, model):
|
def contains(self, model: ModelBase) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a model is registered with auditlog.
|
Check if a model is registered with auditlog.
|
||||||
|
|
||||||
:param model: The model to check.
|
:param model: The model to check.
|
||||||
:type model: Model
|
|
||||||
:return: Whether the model has been registered.
|
:return: Whether the model has been registered.
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
return model in self._registry
|
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.
|
Unregister a model with auditlog. This will not affect the database.
|
||||||
|
|
||||||
:param model: The model to unregister.
|
:param model: The model to unregister.
|
||||||
:type model: Model
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
del self._registry[model]
|
del self._registry[model]
|
||||||
|
|
@ -86,6 +96,19 @@ class AuditlogModelRegistry(object):
|
||||||
else:
|
else:
|
||||||
self._disconnect_signals(model)
|
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):
|
def _connect_signals(self, model):
|
||||||
"""
|
"""
|
||||||
Connect signals for the model.
|
Connect signals for the model.
|
||||||
|
|
@ -101,24 +124,11 @@ class AuditlogModelRegistry(object):
|
||||||
for signal, receiver in self._signals.items():
|
for signal, receiver in self._signals.items():
|
||||||
signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model))
|
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.
|
Generate a dispatch_uid.
|
||||||
"""
|
"""
|
||||||
return (self.__class__, model, signal)
|
return self.__hash__(), model.__qualname__, signal.__hash__()
|
||||||
|
|
||||||
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.")
|
|
||||||
|
|
||||||
|
|
||||||
auditlog = AuditlogModelRegistry()
|
auditlog = AuditlogModelRegistry()
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
Settings file for the Auditlog test suite.
|
Settings file for the Auditlog test suite.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import django
|
|
||||||
|
|
||||||
SECRET_KEY = 'test'
|
SECRET_KEY = 'test'
|
||||||
|
|
||||||
|
|
@ -17,32 +16,22 @@ INSTALLED_APPS = [
|
||||||
'multiselectfield',
|
'multiselectfield',
|
||||||
]
|
]
|
||||||
|
|
||||||
middlewares = (
|
MIDDLEWARE = [
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'auditlog.middleware.AuditlogMiddleware',
|
'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 = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': POSTGRES_DRIVER,
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'NAME': 'auditlog_tests_db',
|
'NAME': os.getenv('TEST_DB_NAME', 'auditlog' + os.environ.get("TOX_PARALLEL_ENV", "")),
|
||||||
'USER': 'postgres',
|
'USER': os.getenv('TEST_DB_USER', 'postgres'),
|
||||||
'PASSWORD': '',
|
'PASSWORD': os.getenv('TEST_DB_PASS', ''),
|
||||||
'HOST': '127.0.0.1',
|
'HOST': os.getenv('TEST_DB_HOST', '127.0.0.1'),
|
||||||
'PORT': '5432',
|
'PORT': os.getenv('TEST_DB_PORT', '5432'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import datetime
|
import datetime
|
||||||
import django
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth
|
|
||||||
from django.contrib.auth.models import User, AnonymousUser
|
from django.contrib.auth.models import User, AnonymousUser
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
from django.utils import dateformat, formats, timezone
|
from django.utils import dateformat, formats, timezone
|
||||||
from dateutil.tz import gettz
|
from dateutil.tz import gettz
|
||||||
|
import mock
|
||||||
|
|
||||||
from auditlog.middleware import AuditlogMiddleware
|
from auditlog.middleware import AuditlogMiddleware
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
|
|
@ -17,7 +14,6 @@ from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKe
|
||||||
ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \
|
ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \
|
||||||
ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \
|
ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \
|
||||||
CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel
|
CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel
|
||||||
from auditlog import compat
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleModelTest(TestCase):
|
class SimpleModelTest(TestCase):
|
||||||
|
|
@ -121,66 +117,64 @@ class MiddlewareTest(TestCase):
|
||||||
Test the middleware responsible for connecting and disconnecting the signals used in automatic logging.
|
Test the middleware responsible for connecting and disconnecting the signals used in automatic logging.
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
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.factory = RequestFactory()
|
||||||
self.user = User.objects.create_user(username='test', email='test@example.com', password='top_secret')
|
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):
|
def test_request_anonymous(self):
|
||||||
"""No actor will be logged when a user is not logged in."""
|
"""No actor will be logged when a user is not logged in."""
|
||||||
# Create a request
|
|
||||||
request = self.factory.get('/')
|
request = self.factory.get('/')
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
|
|
||||||
# Run middleware
|
self.get_response_mock.side_effect = self.side_effect(self.assert_no_listeners)
|
||||||
self.middleware.process_request(request)
|
|
||||||
|
|
||||||
# Validate result
|
response = self.middleware(request)
|
||||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
|
||||||
|
|
||||||
# Finalize transaction
|
self.assertIs(response, self.response_mock)
|
||||||
self.middleware.process_exception(request, None)
|
self.get_response_mock.assert_called_once_with(request)
|
||||||
|
self.assert_no_listeners()
|
||||||
|
|
||||||
def test_request(self):
|
def test_request(self):
|
||||||
"""The actor will be logged when a user is logged in."""
|
"""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 = self.factory.get('/')
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
# Run middleware
|
self.get_response_mock.side_effect = self.side_effect(self.assert_has_listeners)
|
||||||
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())
|
|
||||||
|
|
||||||
# Validate result
|
response = self.middleware(request)
|
||||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
|
||||||
|
self.assertIs(response, self.response_mock)
|
||||||
|
self.get_response_mock.assert_called_once_with(request)
|
||||||
|
self.assert_no_listeners()
|
||||||
|
|
||||||
def test_exception(self):
|
def test_exception(self):
|
||||||
"""The signal will be disconnected when an exception is raised."""
|
"""The signal will be disconnected when an exception is raised."""
|
||||||
# Create a request
|
|
||||||
request = self.factory.get('/')
|
request = self.factory.get('/')
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
# Run middleware
|
SomeException = type('SomeException', (Exception,), {})
|
||||||
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"))
|
|
||||||
|
|
||||||
# Validate result
|
self.get_response_mock.side_effect = SomeException
|
||||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
|
||||||
|
with self.assertRaises(SomeException):
|
||||||
|
self.middleware(request)
|
||||||
|
|
||||||
|
self.assert_no_listeners()
|
||||||
|
|
||||||
|
|
||||||
class SimpeIncludeModelTest(TestCase):
|
class SimpeIncludeModelTest(TestCase):
|
||||||
|
|
@ -603,44 +597,6 @@ class PostgresArrayFieldModelTest(TestCase):
|
||||||
msg="The human readable text 'Green' is displayed.")
|
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):
|
class AdminPanelTest(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
7
auditlog_tests/urls.py
Normal file
7
auditlog_tests/urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^admin/', admin.site.urls),
|
||||||
|
]
|
||||||
185
docs/Makefile
185
docs/Makefile
|
|
@ -1,177 +1,20 @@
|
||||||
# Makefile for Sphinx documentation
|
# Minimal makefile for Sphinx documentation
|
||||||
#
|
#
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
# You can set these variables from the command line, and also
|
||||||
SPHINXOPTS =
|
# from the environment for the first two.
|
||||||
SPHINXBUILD = sphinx-build
|
SPHINXOPTS ?=
|
||||||
PAPER =
|
SPHINXBUILD ?= sphinx-build
|
||||||
BUILDDIR = build
|
SOURCEDIR = source
|
||||||
|
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
|
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
help:
|
help:
|
||||||
@echo "Please use \`make <target>' where <target> is one of"
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
@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)"
|
|
||||||
|
|
||||||
clean:
|
.PHONY: help Makefile
|
||||||
rm -rf $(BUILDDIR)/*
|
|
||||||
|
|
||||||
html:
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
@echo
|
%: Makefile
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
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."
|
|
||||||
|
|
|
||||||
277
docs/make.bat
277
docs/make.bat
|
|
@ -1,242 +1,35 @@
|
||||||
@ECHO OFF
|
@ECHO OFF
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
pushd %~dp0
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
REM Command file for Sphinx documentation
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
if "%SPHINXBUILD%" == "" (
|
||||||
set BUILDDIR=build
|
set SPHINXBUILD=sphinx-build
|
||||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
|
)
|
||||||
set I18NSPHINXOPTS=%SPHINXOPTS% source
|
set SOURCEDIR=source
|
||||||
if NOT "%PAPER%" == "" (
|
set BUILDDIR=_build
|
||||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
|
||||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
if "%1" == "" goto help
|
||||||
)
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
if "%1" == "" goto help
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
if "%1" == "help" (
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
:help
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
echo. html to make standalone HTML files
|
echo.may add the Sphinx directory to PATH.
|
||||||
echo. dirhtml to make HTML files named index.html in directories
|
echo.
|
||||||
echo. singlehtml to make a single large HTML file
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
echo. pickle to make pickle files
|
echo.http://sphinx-doc.org/
|
||||||
echo. json to make JSON files
|
exit /b 1
|
||||||
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
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
echo. epub to make an epub
|
goto end
|
||||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
|
||||||
echo. text to make text files
|
:help
|
||||||
echo. man to make manual pages
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
echo. texinfo to make Texinfo files
|
|
||||||
echo. gettext to make PO message catalogs
|
:end
|
||||||
echo. changes to make an overview over all changed/added/deprecated items
|
popd
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,40 @@
|
||||||
# -*- coding: utf-8 -*-
|
# Configuration file for the Sphinx documentation builder.
|
||||||
#
|
#
|
||||||
# django-auditlog documentation build configuration file, created by
|
# This file only contains a selection of the most common options. For a full
|
||||||
# sphinx-quickstart on Wed Nov 6 20:39:48 2013.
|
# list see the documentation:
|
||||||
#
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
# This file is execfile()d with the current directory set to its
|
|
||||||
# containing dir.
|
# -- Preliminary -------------------------------------------------------------
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
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,
|
# 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
|
# 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.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
sys.path.insert(0, os.path.abspath('../../src'))
|
|
||||||
|
|
||||||
# Django settings
|
# Add sources folder
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproject.settings')
|
sys.path.insert(0, os.path.abspath('../../'))
|
||||||
from django.conf import settings
|
|
||||||
settings.configure()
|
|
||||||
|
|
||||||
# -- 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.
|
# -- Project information -----------------------------------------------------
|
||||||
#needs_sphinx = '1.0'
|
|
||||||
|
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
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
|
@ -39,229 +44,25 @@ extensions = [
|
||||||
'sphinx.ext.viewcode',
|
'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.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
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
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# 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
|
# -- Options for HTML output -------------------------------------------------
|
||||||
# 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 ----------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
html_theme = 'sphinx_rtd_theme'
|
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,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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
|
| GitHub repository: https://github.com/jazzband/django-auditlog
|
||||||
| Issues: https://github.com/jjkester/django-auditlog/issues
|
| Issues: https://github.com/jazzband/django-auditlog/issues
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@ way to do this is by using the Python Package Index (PyPI). Simply run the follo
|
||||||
``pip install django-auditlog``
|
``pip install django-auditlog``
|
||||||
|
|
||||||
Instead of installing Auditlog via PyPI, you can also clone the Git repository or download the source code via GitHub.
|
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**
|
**Requirements**
|
||||||
|
|
||||||
- Python 2.7, 3.4 or higher
|
- Python 3.5 or higher
|
||||||
- Django 1.8 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
|
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/jjkester/django-auditlog.
|
at https://travis-ci.org/jazzband/django-auditlog.
|
||||||
|
|
||||||
Adding Auditlog to your Django application
|
Adding Auditlog to your Django application
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,8 @@ Management commands
|
||||||
|
|
||||||
Auditlog provides the ``auditlogflush`` management command to clear all log entries from the database.
|
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
|
By default, the command asks for confirmation. It is possible to run the command with the `-y` or `--yes` flag to skip
|
||||||
input.
|
confirmation and immediately delete all entries.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,2 +1,15 @@
|
||||||
django-jsonfield>=1.0.0
|
# Build requirements
|
||||||
python-dateutil==2.6.0
|
setuptools
|
||||||
|
wheel
|
||||||
|
|
||||||
|
# Docs requirements
|
||||||
|
sphinx
|
||||||
|
sphinx_rtd_theme
|
||||||
|
|
||||||
|
# Test requirements
|
||||||
|
coverage
|
||||||
|
tox
|
||||||
|
codecov
|
||||||
|
django-multiselectfield
|
||||||
|
mock
|
||||||
|
psycopg2-binary
|
||||||
|
|
|
||||||
27
setup.py
27
setup.py
|
|
@ -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(
|
setup(
|
||||||
name='django-auditlog',
|
name='django-auditlog',
|
||||||
version='0.4.7',
|
version=auditlog.__version__,
|
||||||
packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'],
|
packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'],
|
||||||
package_dir={'': 'src'},
|
url='https://github.com/MacmillanPlatform/django-auditlog/',
|
||||||
url='https://github.com/jjkester/django-auditlog',
|
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Jan-Jelle Kester',
|
author='Jan-Jelle Kester',
|
||||||
|
maintainer='Alieh Rymašeŭski',
|
||||||
description='Audit log app for Django',
|
description='Audit log app for Django',
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
'django-admin-rangefilter>=0.5.0',
|
||||||
'django-jsonfield>=1.0.0',
|
'django-jsonfield>=1.0.0',
|
||||||
'python-dateutil==2.6.0'
|
'python-dateutil>=2.6.0',
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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']
|
|
||||||
|
|
@ -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
19
tox.ini
|
|
@ -1,23 +1,18 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
{py27,py34,py35,py36,py37}-django-111
|
{py35,py36,py37,py38}-django-22
|
||||||
{py34,py35,py36,py37}-django-20
|
{py36,py37,py38}-django-30
|
||||||
{py35,py36,py37}-django-21
|
|
||||||
{py35,py36,py37}-django-22
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
setenv =
|
setenv =
|
||||||
PYTHONPATH = {toxinidir}:{toxinidir}/src/auditlog
|
PYTHONPATH = {toxinidir}:{toxinidir}/auditlog
|
||||||
commands = coverage run --source src/auditlog src/runtests.py
|
commands = coverage run -p --source auditlog runtests.py
|
||||||
deps =
|
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
|
django-22: Django>=2.2,<2.3
|
||||||
-r{toxinidir}/requirements-test.txt
|
django-30: Django>=3.0,<3.1
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
basepython =
|
basepython =
|
||||||
|
py38: python3.8
|
||||||
py37: python3.7
|
py37: python3.7
|
||||||
py36: python3.6
|
py36: python3.6
|
||||||
py35: python3.5
|
py35: python3.5
|
||||||
py34: python3.4
|
|
||||||
py27: python2.7
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue