mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-03-17 06:30:27 +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
|
||||
*.egg-info
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
.idea
|
||||
.project
|
||||
.pydevproject
|
||||
.coverage
|
||||
venv/
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
### JetBrains
|
||||
.idea/
|
||||
|
|
|
|||
43
.travis.yml
43
.travis.yml
|
|
@ -11,44 +11,29 @@ addons:
|
|||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django-111
|
||||
|
||||
- python: 3.4
|
||||
env: TOXENV=py34-django-111
|
||||
- python: 3.4
|
||||
env: TOXENV=py34-django-20
|
||||
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django-111
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django-20
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django-21
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django-22
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django-30
|
||||
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django-111
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django-20
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django-21
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django-22
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django-30
|
||||
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-django-111
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-django-20
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-django-21
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-django-22
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-django-30
|
||||
|
||||
- python: 3.8
|
||||
env: TOXENV=py38-django-22
|
||||
- python: 3.8
|
||||
env: TOXENV=py38-django-30
|
||||
|
||||
fast_finish: true
|
||||
|
||||
install: pip install -r requirements-test.txt
|
||||
install: pip install -r requirements.txt
|
||||
|
||||
script: tox
|
||||
|
||||
|
|
@ -59,7 +44,7 @@ deploy:
|
|||
provider: pypi
|
||||
# PyPI credentials supplied with environment variables from repository settings
|
||||
on:
|
||||
repo: jjkester/django-auditlog
|
||||
repo: jazzband/django-auditlog
|
||||
branch: stable
|
||||
condition: $TOXENV = py36-django-20
|
||||
condition: $TOXENV = py38-django-30
|
||||
edge: true
|
||||
|
|
|
|||
3
CONTRIBUTING.md
Normal file
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)
|
||||
|
||||
Copyright (c) 2015 Jan-Jelle Kester
|
||||
Copyright (c) 2013-2020 Jan-Jelle Kester
|
||||
Copyright (c) 2019-2020 Alieh Rymašeuski <alieh.rymasheuski@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
|
|
|||
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
|
||||
===============
|
||||
|
||||
[](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)
|
||||
|
||||
**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'
|
||||
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
|
||||
|
||||
|
||||
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.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Model, NOT_PROVIDED, DateTimeField
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
|
||||
def track_field(field):
|
||||
|
|
@ -27,10 +25,6 @@ def track_field(field):
|
|||
if getattr(field, 'remote_field', None) is not None and field.remote_field.model == LogEntry:
|
||||
return False
|
||||
|
||||
# 1.8 check
|
||||
elif getattr(field, 'rel', None) is not None and field.rel.to == LogEntry:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -46,17 +40,13 @@ def get_fields_in_model(instance):
|
|||
"""
|
||||
assert isinstance(instance, Model)
|
||||
|
||||
# Check if the Django 1.8 _meta API is available
|
||||
use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields)
|
||||
|
||||
if use_api:
|
||||
return [f for f in instance._meta.get_fields() if track_field(f)]
|
||||
return instance._meta.fields
|
||||
return [f for f in instance._meta.get_fields() if track_field(f)]
|
||||
|
||||
|
||||
def get_field_value(obj, field):
|
||||
"""
|
||||
Gets the value of a given model instance field.
|
||||
|
||||
:param obj: The model instance.
|
||||
:type obj: Model
|
||||
:param field: The field you want to find the value of.
|
||||
|
|
@ -66,7 +56,7 @@ def get_field_value(obj, field):
|
|||
"""
|
||||
if isinstance(field, DateTimeField):
|
||||
# DateTimeFields are timezone-aware, so we need to convert the field
|
||||
# to its naive form before we can accuratly compare them for changes.
|
||||
# to its naive form before we can accurately compare them for changes.
|
||||
try:
|
||||
value = field.to_python(getattr(obj, field.name, None))
|
||||
if value is not None and settings.USE_TZ and not timezone.is_naive(value):
|
||||
|
|
@ -75,7 +65,7 @@ def get_field_value(obj, field):
|
|||
value = field.default if field.default is not NOT_PROVIDED else None
|
||||
else:
|
||||
try:
|
||||
value = smart_text(getattr(obj, field.name, None))
|
||||
value = smart_str(getattr(obj, field.name, None))
|
||||
except ObjectDoesNotExist:
|
||||
value = field.default if field.default is not NOT_PROVIDED else None
|
||||
|
||||
|
|
@ -97,9 +87,9 @@ def model_instance_diff(old, new):
|
|||
"""
|
||||
from auditlog.registry import auditlog
|
||||
|
||||
if not(old is None or isinstance(old, Model)):
|
||||
if not (old is None or isinstance(old, Model)):
|
||||
raise TypeError("The supplied old instance is not a valid model instance.")
|
||||
if not(new is None or isinstance(new, Model)):
|
||||
if not (new is None or isinstance(new, Model)):
|
||||
raise TypeError("The supplied new instance is not a valid model instance.")
|
||||
|
||||
diff = {}
|
||||
|
|
@ -135,7 +125,7 @@ def model_instance_diff(old, new):
|
|||
new_value = get_field_value(new, field)
|
||||
|
||||
if old_value != new_value:
|
||||
diff[field.name] = (smart_text(old_value), smart_text(new_value))
|
||||
diff[field.name] = (smart_str(old_value), smart_str(new_value))
|
||||
|
||||
if len(diff) == 0:
|
||||
diff = None
|
||||
96
auditlog/filters.py
Normal file
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
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
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
|
||||
|
||||
from django import urls as urlresolvers
|
||||
from django.conf import settings
|
||||
try:
|
||||
from django.core import urlresolvers
|
||||
except ImportError:
|
||||
from django import urls as urlresolvers
|
||||
try:
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
except ImportError:
|
||||
from django.core.urlresolvers import NoReverseMatch
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import localtime
|
||||
|
||||
from auditlog.models import LogEntry
|
||||
|
||||
MAX = 75
|
||||
|
||||
|
|
@ -18,7 +15,8 @@ MAX = 75
|
|||
class LogEntryAdminMixin(object):
|
||||
|
||||
def created(self, obj):
|
||||
return obj.timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
||||
return localtime(obj.timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
created.short_description = 'Created'
|
||||
|
||||
def user_url(self, obj):
|
||||
|
|
@ -26,12 +24,13 @@ class LogEntryAdminMixin(object):
|
|||
app_label, model = settings.AUTH_USER_MODEL.split('.')
|
||||
viewname = 'admin:%s_%s_change' % (app_label, model.lower())
|
||||
try:
|
||||
link = urlresolvers.reverse(viewname, args=[obj.actor.id])
|
||||
link = urlresolvers.reverse(viewname, args=[obj.actor.pk])
|
||||
except NoReverseMatch:
|
||||
return u'%s' % (obj.actor)
|
||||
return format_html(u'<a href="{}">{}</a>', link, obj.actor)
|
||||
|
||||
return 'system'
|
||||
|
||||
user_url.short_description = 'User'
|
||||
|
||||
def resource_url(self, obj):
|
||||
|
|
@ -44,10 +43,11 @@ class LogEntryAdminMixin(object):
|
|||
return obj.object_repr
|
||||
else:
|
||||
return format_html(u'<a href="{}">{}</a>', link, obj.object_repr)
|
||||
|
||||
resource_url.short_description = 'Resource'
|
||||
|
||||
def msg_short(self, obj):
|
||||
if obj.action == 2:
|
||||
if obj.action == LogEntry.Action.DELETE:
|
||||
return '' # delete
|
||||
changes = json.loads(obj.changes)
|
||||
s = '' if len(changes) == 1 else 's'
|
||||
|
|
@ -56,10 +56,11 @@ class LogEntryAdminMixin(object):
|
|||
i = fields.rfind(' ', 0, MAX)
|
||||
fields = fields[:i] + ' ..'
|
||||
return '%d change%s: %s' % (len(changes), s, fields)
|
||||
|
||||
msg_short.short_description = 'Changes'
|
||||
|
||||
def msg(self, obj):
|
||||
if obj.action == 2:
|
||||
if obj.action == LogEntry.Action.DELETE:
|
||||
return '' # delete
|
||||
changes = json.loads(obj.changes)
|
||||
msg = '<table><tr><th>#</th><th>Field</th><th>From</th><th>To</th></tr>'
|
||||
|
|
@ -69,4 +70,5 @@ class LogEntryAdminMixin(object):
|
|||
|
||||
msg += '</table>'
|
||||
return mark_safe(msg)
|
||||
|
||||
msg.short_description = 'Changes'
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import ast
|
||||
import json
|
||||
|
||||
from dateutil import parser
|
||||
from dateutil.tz import gettz
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
|
@ -10,13 +10,9 @@ from django.core.exceptions import FieldDoesNotExist
|
|||
from django.db import models, DEFAULT_DB_ALIAS
|
||||
from django.db.models import QuerySet, Q
|
||||
from django.utils import formats, timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
from django.utils.six import iteritems, integer_types
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jsonfield.fields import JSONField
|
||||
from dateutil import parser
|
||||
from dateutil.tz import gettz
|
||||
|
||||
|
||||
class LogEntryManager(models.Manager):
|
||||
|
|
@ -41,9 +37,9 @@ class LogEntryManager(models.Manager):
|
|||
if changes is not None:
|
||||
kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance))
|
||||
kwargs.setdefault('object_pk', pk)
|
||||
kwargs.setdefault('object_repr', smart_text(instance))
|
||||
kwargs.setdefault('object_repr', smart_str(instance))
|
||||
|
||||
if isinstance(pk, integer_types):
|
||||
if isinstance(pk, int):
|
||||
kwargs.setdefault('object_id', pk)
|
||||
|
||||
get_additional_data = getattr(instance, 'get_additional_data', None)
|
||||
|
|
@ -78,10 +74,10 @@ class LogEntryManager(models.Manager):
|
|||
content_type = ContentType.objects.get_for_model(instance.__class__)
|
||||
pk = self._get_pk_value(instance)
|
||||
|
||||
if isinstance(pk, integer_types):
|
||||
if isinstance(pk, int):
|
||||
return self.filter(content_type=content_type, object_id=pk)
|
||||
else:
|
||||
return self.filter(content_type=content_type, object_pk=smart_text(pk))
|
||||
return self.filter(content_type=content_type, object_pk=smart_str(pk))
|
||||
|
||||
def get_for_objects(self, queryset):
|
||||
"""
|
||||
|
|
@ -98,10 +94,10 @@ class LogEntryManager(models.Manager):
|
|||
content_type = ContentType.objects.get_for_model(queryset.model)
|
||||
primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True))
|
||||
|
||||
if isinstance(primary_keys[0], integer_types):
|
||||
if isinstance(primary_keys[0], int):
|
||||
return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct()
|
||||
elif isinstance(queryset.model._meta.pk, models.UUIDField):
|
||||
primary_keys = [smart_text(pk) for pk in primary_keys]
|
||||
primary_keys = [smart_str(pk) for pk in primary_keys]
|
||||
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
|
||||
else:
|
||||
return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct()
|
||||
|
|
@ -140,7 +136,6 @@ class LogEntryManager(models.Manager):
|
|||
return pk
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class LogEntry(models.Model):
|
||||
"""
|
||||
Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available)
|
||||
|
|
@ -189,6 +184,7 @@ class LogEntry(models.Model):
|
|||
ordering = ['-timestamp']
|
||||
verbose_name = _("log entry")
|
||||
verbose_name_plural = _("log entries")
|
||||
index_together = (("timestamp", "id"))
|
||||
|
||||
def __str__(self):
|
||||
if self.action == self.Action.CREATE:
|
||||
|
|
@ -213,7 +209,7 @@ class LogEntry(models.Model):
|
|||
return {}
|
||||
|
||||
@property
|
||||
def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '):
|
||||
def changes_str(self, colon=': ', arrow=' \u2192 ', separator='; '):
|
||||
"""
|
||||
Return the changes recorded in this log entry as a string. The formatting of the string can be customized by
|
||||
setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use
|
||||
|
|
@ -226,8 +222,8 @@ class LogEntry(models.Model):
|
|||
"""
|
||||
substrings = []
|
||||
|
||||
for field, values in iteritems(self.changes_dict):
|
||||
substring = smart_text('{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format(
|
||||
for field, values in self.changes_dict.items():
|
||||
substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format(
|
||||
field_name=field,
|
||||
colon=colon,
|
||||
old=values[0],
|
||||
|
|
@ -249,7 +245,7 @@ class LogEntry(models.Model):
|
|||
model_fields = auditlog.get_model_fields(model._meta.model)
|
||||
changes_display_dict = {}
|
||||
# grab the changes_dict and iterate through
|
||||
for field_name, values in iteritems(self.changes_dict):
|
||||
for field_name, values in self.changes_dict.items():
|
||||
# try to get the field attribute on the model
|
||||
try:
|
||||
field = model._meta.get_field(field_name)
|
||||
|
|
@ -259,9 +255,9 @@ class LogEntry(models.Model):
|
|||
values_display = []
|
||||
# handle choices fields and Postgres ArrayField to get human readable version
|
||||
choices_dict = None
|
||||
if hasattr(field, 'choices') and len(field.choices) > 0:
|
||||
if getattr(field, 'choices', []):
|
||||
choices_dict = dict(field.choices)
|
||||
if hasattr(field, 'base_field') and getattr(field.base_field, 'choices', False):
|
||||
if getattr(getattr(field, 'base_field', None), 'choices', []):
|
||||
choices_dict = dict(field.base_field.choices)
|
||||
|
||||
if choices_dict:
|
||||
|
|
@ -355,6 +351,7 @@ class AuditlogHistoryField(GenericRelation):
|
|||
# South compatibility for AuditlogHistoryField
|
||||
try:
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
|
||||
add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"])
|
||||
raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.")
|
||||
except ImportError:
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
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.utils.six import iteritems
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db.models.signals import pre_save, post_save, post_delete, ModelSignal
|
||||
|
||||
DispatchUID = Tuple[int, str, int]
|
||||
|
||||
|
||||
class AuditlogModelRegistry(object):
|
||||
"""
|
||||
A registry that keeps track of the models that use Auditlog to track changes.
|
||||
"""
|
||||
def __init__(self, create=True, update=True, delete=True, custom=None):
|
||||
|
||||
def __init__(self, create: bool = True, update: bool = True, delete: bool = True,
|
||||
custom: Optional[Dict[ModelSignal, Callable]] = None):
|
||||
from auditlog.receivers import log_create, log_update, log_delete
|
||||
|
||||
self._registry = {}
|
||||
|
|
@ -25,17 +29,25 @@ class AuditlogModelRegistry(object):
|
|||
if custom is not None:
|
||||
self._signals.update(custom)
|
||||
|
||||
def register(self, model=None, include_fields=[], exclude_fields=[], mapping_fields={}):
|
||||
def register(self, model: ModelBase = None, include_fields: Optional[List[str]] = None,
|
||||
exclude_fields: Optional[List[str]] = None, mapping_fields: Optional[Dict[str, str]] = None):
|
||||
"""
|
||||
Register a model with auditlog. Auditlog will then track mutations on this model's instances.
|
||||
|
||||
:param model: The model to register.
|
||||
:type model: Model
|
||||
:param include_fields: The fields to include. Implicitly excludes all other fields.
|
||||
:type include_fields: list
|
||||
:param exclude_fields: The fields to exclude. Overrides the fields to include.
|
||||
:type exclude_fields: list
|
||||
:param mapping_fields: Mapping from field names to strings in diff.
|
||||
|
||||
"""
|
||||
|
||||
if include_fields is None:
|
||||
include_fields = []
|
||||
if exclude_fields is None:
|
||||
exclude_fields = []
|
||||
if mapping_fields is None:
|
||||
mapping_fields = {}
|
||||
|
||||
def registrar(cls):
|
||||
"""Register models for a given class."""
|
||||
if not issubclass(cls, Model):
|
||||
|
|
@ -61,23 +73,21 @@ class AuditlogModelRegistry(object):
|
|||
# Otherwise, just register the model.
|
||||
registrar(model)
|
||||
|
||||
def contains(self, model):
|
||||
def contains(self, model: ModelBase) -> bool:
|
||||
"""
|
||||
Check if a model is registered with auditlog.
|
||||
|
||||
:param model: The model to check.
|
||||
:type model: Model
|
||||
:return: Whether the model has been registered.
|
||||
:rtype: bool
|
||||
"""
|
||||
return model in self._registry
|
||||
|
||||
def unregister(self, model):
|
||||
def unregister(self, model: ModelBase) -> None:
|
||||
"""
|
||||
Unregister a model with auditlog. This will not affect the database.
|
||||
|
||||
:param model: The model to unregister.
|
||||
:type model: Model
|
||||
"""
|
||||
try:
|
||||
del self._registry[model]
|
||||
|
|
@ -86,6 +96,19 @@ class AuditlogModelRegistry(object):
|
|||
else:
|
||||
self._disconnect_signals(model)
|
||||
|
||||
def get_models(self) -> List[ModelBase]:
|
||||
"""Get a list of all registered models."""
|
||||
return list(self._registry.keys())
|
||||
|
||||
list = get_models # TODO: remove after all users migrate to get_models
|
||||
|
||||
def get_model_fields(self, model: ModelBase):
|
||||
return {
|
||||
'include_fields': list(self._registry[model]['include_fields']),
|
||||
'exclude_fields': list(self._registry[model]['exclude_fields']),
|
||||
'mapping_fields': dict(self._registry[model]['mapping_fields']),
|
||||
}
|
||||
|
||||
def _connect_signals(self, model):
|
||||
"""
|
||||
Connect signals for the model.
|
||||
|
|
@ -101,24 +124,11 @@ class AuditlogModelRegistry(object):
|
|||
for signal, receiver in self._signals.items():
|
||||
signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model))
|
||||
|
||||
def _dispatch_uid(self, signal, model):
|
||||
def _dispatch_uid(self, signal, model) -> DispatchUID:
|
||||
"""
|
||||
Generate a dispatch_uid.
|
||||
"""
|
||||
return (self.__class__, model, signal)
|
||||
|
||||
def get_model_fields(self, model):
|
||||
return {
|
||||
'include_fields': self._registry[model]['include_fields'],
|
||||
'exclude_fields': self._registry[model]['exclude_fields'],
|
||||
'mapping_fields': self._registry[model]['mapping_fields'],
|
||||
}
|
||||
|
||||
|
||||
class AuditLogModelRegistry(AuditlogModelRegistry):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AuditLogModelRegistry, self).__init__(*args, **kwargs)
|
||||
raise DeprecationWarning("Use AuditlogModelRegistry instead of AuditLogModelRegistry, AuditLogModelRegistry will be removed in django-auditlog 0.4.0 or later.")
|
||||
return self.__hash__(), model.__qualname__, signal.__hash__()
|
||||
|
||||
|
||||
auditlog = AuditlogModelRegistry()
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
Settings file for the Auditlog test suite.
|
||||
"""
|
||||
import os
|
||||
import django
|
||||
|
||||
SECRET_KEY = 'test'
|
||||
|
||||
|
|
@ -17,32 +16,22 @@ INSTALLED_APPS = [
|
|||
'multiselectfield',
|
||||
]
|
||||
|
||||
middlewares = (
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'auditlog.middleware.AuditlogMiddleware',
|
||||
)
|
||||
|
||||
if django.VERSION < (1, 10):
|
||||
MIDDLEWARE_CLASSES = middlewares
|
||||
else:
|
||||
MIDDLEWARE = middlewares
|
||||
|
||||
if django.VERSION <= (1, 9):
|
||||
POSTGRES_DRIVER = 'django.db.backends.postgresql_psycopg2'
|
||||
else:
|
||||
POSTGRES_DRIVER = 'django.db.backends.postgresql'
|
||||
]
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': POSTGRES_DRIVER,
|
||||
'NAME': 'auditlog_tests_db',
|
||||
'USER': 'postgres',
|
||||
'PASSWORD': '',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('TEST_DB_NAME', 'auditlog' + os.environ.get("TOX_PARALLEL_ENV", "")),
|
||||
'USER': os.getenv('TEST_DB_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('TEST_DB_PASS', ''),
|
||||
'HOST': os.getenv('TEST_DB_HOST', '127.0.0.1'),
|
||||
'PORT': os.getenv('TEST_DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
import datetime
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.signals import pre_save
|
||||
from django.http import HttpResponse
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.utils import dateformat, formats, timezone
|
||||
from dateutil.tz import gettz
|
||||
import mock
|
||||
|
||||
from auditlog.middleware import AuditlogMiddleware
|
||||
from auditlog.models import LogEntry
|
||||
|
|
@ -17,7 +14,6 @@ from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKe
|
|||
ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \
|
||||
ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \
|
||||
CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel
|
||||
from auditlog import compat
|
||||
|
||||
|
||||
class SimpleModelTest(TestCase):
|
||||
|
|
@ -121,66 +117,64 @@ class MiddlewareTest(TestCase):
|
|||
Test the middleware responsible for connecting and disconnecting the signals used in automatic logging.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.middleware = AuditlogMiddleware()
|
||||
self.get_response_mock = mock.Mock()
|
||||
self.response_mock = mock.Mock()
|
||||
self.middleware = AuditlogMiddleware(get_response=self.get_response_mock)
|
||||
self.factory = RequestFactory()
|
||||
self.user = User.objects.create_user(username='test', email='test@example.com', password='top_secret')
|
||||
|
||||
def side_effect(self, assertion):
|
||||
def inner(request):
|
||||
assertion()
|
||||
return self.response_mock
|
||||
|
||||
return inner
|
||||
|
||||
def assert_has_listeners(self):
|
||||
self.assertTrue(pre_save.has_listeners(LogEntry))
|
||||
|
||||
def assert_no_listeners(self):
|
||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
||||
|
||||
def test_request_anonymous(self):
|
||||
"""No actor will be logged when a user is not logged in."""
|
||||
# Create a request
|
||||
request = self.factory.get('/')
|
||||
request.user = AnonymousUser()
|
||||
|
||||
# Run middleware
|
||||
self.middleware.process_request(request)
|
||||
self.get_response_mock.side_effect = self.side_effect(self.assert_no_listeners)
|
||||
|
||||
# Validate result
|
||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
||||
response = self.middleware(request)
|
||||
|
||||
# Finalize transaction
|
||||
self.middleware.process_exception(request, None)
|
||||
self.assertIs(response, self.response_mock)
|
||||
self.get_response_mock.assert_called_once_with(request)
|
||||
self.assert_no_listeners()
|
||||
|
||||
def test_request(self):
|
||||
"""The actor will be logged when a user is logged in."""
|
||||
# Create a request
|
||||
request = self.factory.get('/')
|
||||
request.user = self.user
|
||||
# Run middleware
|
||||
self.middleware.process_request(request)
|
||||
|
||||
# Validate result
|
||||
self.assertTrue(pre_save.has_listeners(LogEntry))
|
||||
|
||||
# Finalize transaction
|
||||
self.middleware.process_exception(request, None)
|
||||
|
||||
def test_response(self):
|
||||
"""The signal will be disconnected when the request is processed."""
|
||||
# Create a request
|
||||
request = self.factory.get('/')
|
||||
request.user = self.user
|
||||
|
||||
# Run middleware
|
||||
self.middleware.process_request(request)
|
||||
self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it.
|
||||
self.middleware.process_response(request, HttpResponse())
|
||||
self.get_response_mock.side_effect = self.side_effect(self.assert_has_listeners)
|
||||
|
||||
# Validate result
|
||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
||||
response = self.middleware(request)
|
||||
|
||||
self.assertIs(response, self.response_mock)
|
||||
self.get_response_mock.assert_called_once_with(request)
|
||||
self.assert_no_listeners()
|
||||
|
||||
def test_exception(self):
|
||||
"""The signal will be disconnected when an exception is raised."""
|
||||
# Create a request
|
||||
request = self.factory.get('/')
|
||||
request.user = self.user
|
||||
|
||||
# Run middleware
|
||||
self.middleware.process_request(request)
|
||||
self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it.
|
||||
self.middleware.process_exception(request, ValidationError("Test"))
|
||||
SomeException = type('SomeException', (Exception,), {})
|
||||
|
||||
# Validate result
|
||||
self.assertFalse(pre_save.has_listeners(LogEntry))
|
||||
self.get_response_mock.side_effect = SomeException
|
||||
|
||||
with self.assertRaises(SomeException):
|
||||
self.middleware(request)
|
||||
|
||||
self.assert_no_listeners()
|
||||
|
||||
|
||||
class SimpeIncludeModelTest(TestCase):
|
||||
|
|
@ -603,44 +597,6 @@ class PostgresArrayFieldModelTest(TestCase):
|
|||
msg="The human readable text 'Green' is displayed.")
|
||||
|
||||
|
||||
class CompatibilityTest(TestCase):
|
||||
"""Test case for compatibility functions."""
|
||||
|
||||
def test_is_authenticated(self):
|
||||
"""Test that the 'is_authenticated' compatibility function is working.
|
||||
|
||||
Bit of explanation: the `is_authenticated` property on request.user is
|
||||
*always* set to 'False' for AnonymousUser, and it is *always* set to
|
||||
'True' for *any* other (i.e. identified/authenticated) user.
|
||||
|
||||
So, the logic of this test is to ensure that compat.is_authenticated()
|
||||
returns the correct value based on whether or not the User is an
|
||||
anonymous user (simulating what goes on in the real request.user).
|
||||
|
||||
"""
|
||||
|
||||
# Test compat.is_authenticated for anonymous users
|
||||
self.user = auth.get_user(self.client)
|
||||
if django.VERSION < (1, 10):
|
||||
assert self.user.is_anonymous()
|
||||
else:
|
||||
assert self.user.is_anonymous
|
||||
assert not compat.is_authenticated(self.user)
|
||||
|
||||
# Setup some other user, which is *not* anonymous, and check
|
||||
# compat.is_authenticated
|
||||
self.user = User.objects.create(
|
||||
username="test.user",
|
||||
email="test.user@mail.com",
|
||||
password="auditlog"
|
||||
)
|
||||
if django.VERSION < (1, 10):
|
||||
assert not self.user.is_anonymous()
|
||||
else:
|
||||
assert not self.user.is_anonymous
|
||||
assert compat.is_authenticated(self.user)
|
||||
|
||||
|
||||
class AdminPanelTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
7
auditlog_tests/urls.py
Normal file
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.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
.PHONY: help Makefile
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-auditlog.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-auditlog.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-auditlog"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-auditlog"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
|
|
|||
277
docs/make.bat
277
docs/make.bat
|
|
@ -1,242 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% source
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-auditlog.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-auditlog.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
|
|
|
|||
|
|
@ -1,35 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# django-auditlog documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Nov 6 20:39:48 2013.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Preliminary -------------------------------------------------------------
|
||||
|
||||
import sys
|
||||
import os
|
||||
import sphinx_rtd_theme
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../../src'))
|
||||
|
||||
# Django settings
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproject.settings')
|
||||
from django.conf import settings
|
||||
settings.configure()
|
||||
# Add sources folder
|
||||
sys.path.insert(0, os.path.abspath('../../'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
# Setup Django for autodoc
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'auditlog_tests.test_settings')
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'django-auditlog'
|
||||
author = 'Jan-Jelle Kester and contributors'
|
||||
copyright = f'2013-{date.today().year}, {author}'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
import auditlog
|
||||
release = auditlog.__version__
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
|
|
@ -39,229 +44,25 @@ extensions = [
|
|||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Master document that contains the root table of contents
|
||||
master_doc = 'index'
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-auditlog'
|
||||
copyright = u'2017, Jan-Jelle Kester and contributors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.4'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.4.7'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-auditlogdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'django-auditlog.tex', u'django-auditlog Documentation',
|
||||
u'Jan-Jelle Kester', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-auditlog', u'django-auditlog Documentation',
|
||||
[u'Jan-Jelle Kester'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'django-auditlog', u'django-auditlog Documentation',
|
||||
u'Jan-Jelle Kester', 'django-auditlog', '',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
|
|
|
|||
|
|
@ -36,5 +36,5 @@ Contribute to Auditlog
|
|||
If you discovered a bug or want to improve the code, please submit an issue and/or pull request via GitHub.
|
||||
Before submitting a new issue, please make sure there is no issue submitted that involves the same problem.
|
||||
|
||||
| GitHub repository: https://github.com/jjkester/django-auditlog
|
||||
| Issues: https://github.com/jjkester/django-auditlog/issues
|
||||
| GitHub repository: https://github.com/jazzband/django-auditlog
|
||||
| Issues: https://github.com/jazzband/django-auditlog/issues
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ way to do this is by using the Python Package Index (PyPI). Simply run the follo
|
|||
``pip install django-auditlog``
|
||||
|
||||
Instead of installing Auditlog via PyPI, you can also clone the Git repository or download the source code via GitHub.
|
||||
The repository can be found at https://github.com/jjkester/django-auditlog/.
|
||||
The repository can be found at https://github.com/jazzband/django-auditlog/.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Python 2.7, 3.4 or higher
|
||||
- Django 1.8 or higher
|
||||
- Python 3.5 or higher
|
||||
- Django 2.2 or higher
|
||||
|
||||
Auditlog is currently tested with Python 2.7 and 3.4 and Django 1.8, 1.9 and 1.10. The latest test report can be found
|
||||
at https://travis-ci.org/jjkester/django-auditlog.
|
||||
Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2 and 3.0. The latest test report can be found
|
||||
at https://travis-ci.org/jazzband/django-auditlog.
|
||||
|
||||
Adding Auditlog to your Django application
|
||||
------------------------------------------
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ Management commands
|
|||
|
||||
Auditlog provides the ``auditlogflush`` management command to clear all log entries from the database.
|
||||
|
||||
The command asks for confirmation, it is not possible to execute the command without giving any form of (simulated) user
|
||||
input.
|
||||
By default, the command asks for confirmation. It is possible to run the command with the `-y` or `--yes` flag to skip
|
||||
confirmation and immediately delete all entries.
|
||||
|
||||
.. warning::
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
python-dateutil==2.6.0
|
||||
# Build requirements
|
||||
setuptools
|
||||
wheel
|
||||
|
||||
# Docs requirements
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
|
||||
# Test requirements
|
||||
coverage
|
||||
tox
|
||||
codecov
|
||||
django-multiselectfield
|
||||
mock
|
||||
psycopg2-binary
|
||||
|
|
|
|||
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(
|
||||
name='django-auditlog',
|
||||
version='0.4.7',
|
||||
version=auditlog.__version__,
|
||||
packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'],
|
||||
package_dir={'': 'src'},
|
||||
url='https://github.com/jjkester/django-auditlog',
|
||||
url='https://github.com/MacmillanPlatform/django-auditlog/',
|
||||
license='MIT',
|
||||
author='Jan-Jelle Kester',
|
||||
maintainer='Alieh Rymašeŭski',
|
||||
description='Audit log app for Django',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
install_requires=[
|
||||
'django-admin-rangefilter>=0.5.0',
|
||||
'django-jsonfield>=1.0.0',
|
||||
'python-dateutil==2.6.0'
|
||||
'python-dateutil>=2.6.0',
|
||||
],
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
envlist =
|
||||
{py27,py34,py35,py36,py37}-django-111
|
||||
{py34,py35,py36,py37}-django-20
|
||||
{py35,py36,py37}-django-21
|
||||
{py35,py36,py37}-django-22
|
||||
{py35,py36,py37,py38}-django-22
|
||||
{py36,py37,py38}-django-30
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}:{toxinidir}/src/auditlog
|
||||
commands = coverage run --source src/auditlog src/runtests.py
|
||||
PYTHONPATH = {toxinidir}:{toxinidir}/auditlog
|
||||
commands = coverage run -p --source auditlog runtests.py
|
||||
deps =
|
||||
django-111: Django>=1.11,<2.0
|
||||
django-20: Django>=2.0,<2.1
|
||||
django-21: Django>=2.1,<2.2
|
||||
django-22: Django>=2.2,<2.3
|
||||
-r{toxinidir}/requirements-test.txt
|
||||
django-30: Django>=3.0,<3.1
|
||||
-r{toxinidir}/requirements.txt
|
||||
basepython =
|
||||
py38: python3.8
|
||||
py37: python3.7
|
||||
py36: python3.6
|
||||
py35: python3.5
|
||||
py34: python3.4
|
||||
py27: python2.7
|
||||
|
|
|
|||
Loading…
Reference in a new issue