Compare commits

...

29 commits

Author SHA1 Message Date
Alieh Rymašeŭski
5cd55ac38c Bump version to 0.6.5 2020-04-07 12:03:09 +03:00
Alieh Rymašeŭski
8397754a20 Replace dumb paginator with a time-limited one 2020-04-07 11:55:55 +03:00
Alieh Rymašeŭski
da49432924 Bump version to 0.6.4 2020-04-07 11:23:48 +03:00
Alieh Rymašeŭski
3af06e13c7 Add a missing line to manifest 2020-04-07 11:17:39 +03:00
Alieh Rymašeŭski
ae57b0c322 Merge upstream changes from jjkester/master
Conflicts:
    setup.py  - conflicting version bump, expected
    src/auditlog_tests/router.py - removed by them
    tox.ini - upstream extended test matrix with dj21 and dj22, we did
              the same but dropped older versions, keepign our variant
2020-04-07 11:13:45 +03:00
Alieh Rymašeŭski
bde49bdb4f Allow newer versions of dateutil 2019-11-25 15:49:00 +03:00
Alieh Rymašeŭski
c66b36c700 Bump version to 0.6.2 2019-08-22 18:24:46 +03:00
Alieh Rymašeŭski
2ae401a04f Shave off two SELECT COUNT(*) from admin page 2019-08-22 18:24:10 +03:00
Alieh Rymašeŭski
5ba554af56 Bump version to 0.6.1 2019-08-22 18:06:48 +03:00
Alieh Rymašeŭski
df2bf0a05c Replace timestamp index with timestamp+id index
This change is supposed to improve performance of admin view.
2019-08-22 18:05:39 +03:00
Alieh Rymašeŭski
05e6b179fd Bump version to 0.6.0 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
07b38a9345 Update references to the maintainer 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
5784247180 Update requirements section in the docs
We've dropped support for Python2 and unsupported versions of Python
and Django.
2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
2a43cff96f Add python3.7 to tox and classifiers 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
b16b1a0df3 Drop Django 1.9 compatibility 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
9152d225bb Remove obsolete versions from setup and tox 2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
c53b766132 Drop python2.7 support
Our dependency jsonfield broke compatibility with python2.7 recently,
and having no real reasons to support python2.7 we just drop it now.
2019-08-22 16:31:08 +03:00
Alieh Rymašeŭski
e35d0f4194 Bump version to 0.5.3 2019-08-21 19:41:02 +03:00
Alieh Rymašeŭski
e60876ae14 Add index for timestamp 2019-08-21 19:30:25 +03:00
Alieh Rymašeŭski
5dbea8a9a1 Bump version to 0.5.2 2019-05-17 12:41:31 +03:00
Alieh Rymašeŭski
cfbc588cc1 Query ContentTypes instead of distinct LogEntry
SELECT DISTINCT app_label, model FROM log_entry is a very expensive
request for longer logs, while we can always get the list of all
tracked models straight from the registry.

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

View file

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

View file

@ -3,7 +3,7 @@ setup.py
src/auditlog/__init__.py
src/auditlog/admin.py
src/auditlog/apps.py
src/auditlog/compat.py
src/auditlog/context.py
src/auditlog/diff.py
src/auditlog/filters.py
src/auditlog/middleware.py
@ -21,4 +21,6 @@ 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/0008_timestamp_index.py
src/auditlog/migrations/0009_timestamp_id_index.py
src/auditlog/migrations/__init__.py

View file

@ -60,9 +60,9 @@ copyright = u'2017, Jan-Jelle Kester and contributors'
# built documents.
#
# The short X.Y version.
version = '0.4'
version = '0.6'
# The full version, including alpha/beta/rc tags.
release = '0.4.7'
release = '0.6.5'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View file

@ -11,8 +11,8 @@ The repository can be found at https://github.com/jjkester/django-auditlog/.
**Requirements**
- Python 2.7, 3.4 or higher
- Django 1.8 or higher
- Python 3.5 or higher
- Django 1.11 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.

View file

@ -5,3 +5,4 @@ tox>=1.7.0
codecov>=2.0.0
django-multiselectfield==0.1.8
psycopg2-binary
mock

View file

@ -2,26 +2,24 @@ from distutils.core import setup
setup(
name='django-auditlog',
version='0.4.7',
version='0.6.5',
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',
install_requires=[
'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',
'License :: OSI Approved :: MIT License',
],
],
)

View file

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

View file

@ -1,9 +1,32 @@
from django.conf import settings
from django.contrib import admin
from django.core.paginator import Paginator
from django.db import connection, transaction, OperationalError
from django.utils.functional import cached_property
from .models import LogEntry
from .mixins import LogEntryAdminMixin
from .filters import ResourceTypeFilter
class TimeLimitedPaginator(Paginator):
"""A PostgreSQL-specific paginator with a hard time limit for total count of pages.
Courtesy of https://medium.com/@hakibenita/optimizing-django-admin-paginator-53c4eb6bfca3
"""
DEFAULT_PAGE_COUNT = 10000
@cached_property
def count(self):
timeout = getattr(settings, 'AUDITLOG_PAGINATOR_TIMEOUT', 500) # ms
with transaction.atomic(), connection.cursor() as cursor:
cursor.execute('SET LOCAL statement_timeout TO %s;', (timeout,))
try:
return super().count
except OperationalError:
return self.per_page * self.DEFAULT_PAGE_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']
@ -13,6 +36,9 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin):
(None, {'fields': ['created', 'user_url', 'resource_url']}),
('Changes', {'fields': ['action', 'msg']}),
]
list_select_related = ['actor', 'content_type']
show_full_result_count = False
paginator = TimeLimitedPaginator
admin.site.register(LogEntry, LogEntryAdmin)

View file

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

View file

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

55
src/auditlog/context.py Normal file
View file

@ -0,0 +1,55 @@
import contextlib
import time
import threading
from django.contrib.auth import get_user_model
from django.db.models.signals import pre_save
from django.utils.functional import curry
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 = curry(_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']

View file

@ -1,5 +1,3 @@
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

View file

@ -1,4 +1,9 @@
from django.contrib.admin import SimpleListFilter
from django.contrib.contenttypes.models import ContentType
from django.db.models import Value
from django.db.models.functions import Concat
from auditlog.registry import auditlog
class ResourceTypeFilter(SimpleListFilter):
@ -6,9 +11,17 @@ class ResourceTypeFilter(SimpleListFilter):
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())
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:

View file

@ -1,5 +1,4 @@
from django.core.management.base import BaseCommand
from six import moves
from auditlog.models import LogEntry
@ -11,7 +10,7 @@ class Command(BaseCommand):
answer = None
while answer not in ['', 'y', 'n']:
answer = moves.input("Are you sure? [y/N]: ").lower().strip()
answer = input("Are you sure? [y/N]: ").lower().strip()
if answer == 'y':
count = LogEntry.objects.all().count()

View file

@ -1,84 +1,35 @@
from __future__ import unicode_literals
import contextlib
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
from auditlog.context import set_actor
threadlocal = threading.local()
@contextlib.contextmanager
def nullcontext():
"""Equivalent to contextlib.nullcontext(None) from Python 3.7."""
yield
class AuditlogMiddleware(MiddlewareMixin):
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 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'),
}
def __init__(self, get_response=None):
self.get_response = get_response
def __call__(self, request):
# 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]
# 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')
# 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)
if hasattr(request, 'user') and request.user.is_authenticated:
context = set_actor(actor=request.user, remote_addr=remote_addr)
else:
context = nullcontext()
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']
with context:
return self.get_response(request)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import json
import ast
@ -11,7 +9,6 @@ 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 jsonfield.fields import JSONField
@ -43,7 +40,7 @@ class LogEntryManager(models.Manager):
kwargs.setdefault('object_pk', pk)
kwargs.setdefault('object_repr', smart_text(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,7 +75,7 @@ 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))
@ -98,7 +95,7 @@ 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]
@ -189,6 +186,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:
@ -226,7 +224,7 @@ class LogEntry(models.Model):
"""
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(
field_name=field,
colon=colon,
@ -249,7 +247,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)

View file

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

View file

@ -1,8 +1,5 @@
from __future__ import unicode_literals
from django.db.models.signals import pre_save, post_save, post_delete
from django.db.models import Model
from django.utils.six import iteritems
class AuditlogModelRegistry(object):
@ -114,6 +111,10 @@ class AuditlogModelRegistry(object):
'mapping_fields': self._registry[model]['mapping_fields'],
}
def list(self):
"""Get a list of all registered models."""
return list(self._registry.keys())
class AuditLogModelRegistry(AuditlogModelRegistry):
def __init__(self, *args, **kwargs):

View file

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

View file

@ -1,7 +1,7 @@
[tox]
envlist =
{py27,py34,py35,py36,py37}-django-111
{py34,py35,py36,py37}-django-20
{py35,py36,py37}-django-111
{py35,py36,py37}-django-20
{py35,py36,py37}-django-21
{py35,py36,py37}-django-22
@ -19,5 +19,3 @@ basepython =
py37: python3.7
py36: python3.6
py35: python3.5
py34: python3.4
py27: python2.7