diff --git a/.travis.yml b/.travis.yml index 331ae917f..a7eb41837 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,9 @@ python: - 3.3 env: - - DJANGO=Django==1.3.7 - - DJANGO=Django==1.4.10 - - DJANGO=Django==1.5.5 - - DJANGO=Django==1.6.2 + - DJANGO=Django==1.4.13 + - DJANGO=Django==1.5.8 + - DJANGO=Django==1.6.5 - DJANGO=https://github.com/django/django/tarball/stable/1.7.x install: @@ -21,16 +20,16 @@ script: coverage run -a setup.py test matrix: exclude: - python: 2.6 - env: DJANGO=Django==1.6.2 + env: DJANGO=Django==1.6.5 - python: 2.6 env: DJANGO=https://github.com/django/django/tarball/stable/1.7.x - python: 3.2 - env: DJANGO=Django==1.3.7 + env: DJANGO=Django==1.4.13 - python: 3.3 - env: DJANGO=Django==1.3.7 - - python: 3.2 - env: DJANGO=Django==1.4.10 - - python: 3.3 - env: DJANGO=Django==1.4.10 + env: DJANGO=Django==1.4.13 + + include: + - python: 3.4 + env: DJANGO=https://github.com/django/django/tarball/stable/1.7.x after_success: coveralls diff --git a/docs/usage.rst b/docs/usage.rst index 87fb915f9..513b6ee84 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -46,6 +46,21 @@ Django tutorial: Now all changes to ``Poll`` and ``Choice`` model instances will be tracked in the database. +The historical models can also track who made each change. To populate +the history user automatically you can add middleware to your Django +settings: + +.. code-block:: python + + MIDDLEWARE_CLASSES = [ + # ... + 'simple_history.middleware.HistoryRequestMiddleware', + ] + +If you do not want to use the middleware, you can explicitly indicate +the user making the change as indicated in the advanced usage +documentation. + .. _admin_integration: Integration with Django Admin diff --git a/simple_history/middleware.py b/simple_history/middleware.py new file mode 100644 index 000000000..b335ae8e0 --- /dev/null +++ b/simple_history/middleware.py @@ -0,0 +1,13 @@ +from . models import HistoricalRecords + + +class HistoryRequestMiddleware(object): + """Expose request to HistoricalRecords. + + This middleware sets request as a local thread variable, making it + available to the model-level utilities to allow tracking of the + authenticated user making a change. + """ + + def process_request(self, request): + HistoricalRecords.thread.request = request diff --git a/simple_history/models.py b/simple_history/models.py index 29ae28c0c..09d971f65 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import threading import copy try: from django.apps import apps # Django >= 1.7 @@ -53,6 +54,8 @@ def python_2_unicode_compatible(klass): class HistoricalRecords(object): + thread = threading.local() + def __init__(self, verbose_name=None, bases=(models.Model,)): self.user_set_verbose_name = verbose_name try: @@ -213,7 +216,7 @@ def post_delete(self, instance, **kwargs): def create_historical_record(self, instance, type): history_date = getattr(instance, '_history_date', now()) - history_user = getattr(instance, '_history_user', None) + history_user = self.get_history_user(instance) manager = getattr(instance, self.manager_name) attrs = {} for field in instance._meta.fields: @@ -221,6 +224,16 @@ def create_historical_record(self, instance, type): manager.create(history_date=history_date, history_type=type, history_user=history_user, **attrs) + def get_history_user(self, instance): + """Get the modifying user from instance or middleware.""" + try: + return instance._history_user + except AttributeError: + try: + return self.thread.request.user + except AttributeError: + return + class ForeignKeyMixin(object): def get_attname(self): diff --git a/simple_history/tests/tests/test_admin.py b/simple_history/tests/tests/test_admin.py index 00c6e15c0..828988ccf 100644 --- a/simple_history/tests/tests/test_admin.py +++ b/simple_history/tests/tests/test_admin.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta from django_webtest import WebTest +from django.test.utils import override_settings from django import VERSION from django.core.urlresolvers import reverse try: @@ -8,6 +9,7 @@ except ImportError: # django 1.4 compatibility from django.contrib.auth.models import User from django.contrib.admin.util import quote +from django.conf import settings from ..models import Book, Person, Poll @@ -156,3 +158,26 @@ def test_historical_user_with_setter(self): self.login() add_page = self.app.get(reverse('admin:tests_paper_add')) add_page.form.submit() + + def test_history_user_not_saved(self): + self.login() + poll = Poll.objects.create(question="why?", pub_date=today) + historical_poll = poll.history.all()[0] + self.assertIsNone( + historical_poll.history_user, + "No way to know of request, history_user should be unset.", + ) + + def test_middleware_saves_user(self): + overridden_settings = { + 'MIDDLEWARE_CLASSES': + settings.MIDDLEWARE_CLASSES + + ['simple_history.middleware.HistoryRequestMiddleware'], + } + with override_settings(**overridden_settings): + self.login() + poll = Poll.objects.create(question="why?", pub_date=today) + historical_poll = poll.history.all()[0] + self.assertEqual(historical_poll.history_user, self.user, + "Middleware should make the request available to " + "retrieve history_user.") diff --git a/tox.ini b/tox.ini index e077c5485..589393600 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,10 @@ [tox] envlist = - py26-1.3, py26-1.4, py26-1.5, py26-1.6, - py27-1.3, py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk, + py26-1.4, py26-1.5, py26-1.6, + py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk, py32-1.5, py32-1.6, py32-1.7, py32-trunk, py33-1.5, py33-1.6, py33-1.7, py33-trunk, + py34-1.7, py34-trunk, docs, flake8 @@ -27,28 +28,22 @@ exclude = __init__.py deps = flake8 commands = flake8 simple_history -[testenv:py26-1.3] -basepython = python2.6 -deps = - django == 1.3.7 - coverage == 3.6 - [testenv:py26-1.4] basepython = python2.6 deps = - django == 1.4.10 + django == 1.4.13 coverage == 3.6 [testenv:py26-1.5] basepython = python2.6 deps = - django == 1.5.5 + django == 1.5.8 coverage == 3.6 [testenv:py26-1.6] basepython = python2.6 deps = - django == 1.6.2 + django == 1.6.5 coverage == 3.6 [testenv:py26-trunk] @@ -57,29 +52,22 @@ deps = https://github.com/django/django/tarball/master coverage == 3.6 - -[testenv:py27-1.3] -basepython = python2.7 -deps = - django == 1.3.7 - coverage == 3.6 - [testenv:py27-1.4] basepython = python2.7 deps = - django == 1.4.10 + django == 1.4.13 coverage == 3.6 [testenv:py27-1.5] basepython = python2.7 deps = - django == 1.5.5 + django == 1.5.8 coverage == 3.6 [testenv:py27-1.6] basepython = python2.7 deps = - django == 1.6.2 + django == 1.6.5 coverage == 3.6 [testenv:py27-1.7] @@ -98,13 +86,13 @@ deps = [testenv:py32-1.5] basepython = python3.2 deps = - django == 1.5.5 + django == 1.5.8 coverage == 3.6 [testenv:py32-1.6] basepython = python3.2 deps = - django == 1.6.2 + django == 1.6.5 coverage == 3.6 [testenv:py32-1.7] @@ -123,13 +111,13 @@ deps = [testenv:py33-1.5] basepython = python3.3 deps = - django == 1.5.5 + django == 1.5.8 coverage == 3.6 [testenv:py33-1.6] basepython = python3.3 deps = - django == 1.6.2 + django == 1.6.5 coverage == 3.6 [testenv:py33-1.7] @@ -143,3 +131,15 @@ basepython = python3.3 deps = https://github.com/django/django/tarball/master coverage == 3.6 + +[testenv:py34-1.7] +basepython = python3.4 +deps = + https://github.com/django/django/tarball/stable/1.7.x + coverage == 3.6 + +[testenv:py34-trunk] +basepython = python3.4 +deps = + https://github.com/django/django/tarball/master + coverage == 3.6