Skip to content

Commit bb23f85

Browse files
committed
Merge pull request #108 from macro1/user-from-request
Set history_user automatically using middleware
2 parents 7635120 + ed130f3 commit bb23f85

File tree

6 files changed

+102
-37
lines changed

6 files changed

+102
-37
lines changed

.travis.yml

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ python:
77
- 3.3
88

99
env:
10-
- DJANGO=Django==1.3.7
11-
- DJANGO=Django==1.4.10
12-
- DJANGO=Django==1.5.5
13-
- DJANGO=Django==1.6.2
10+
- DJANGO=Django==1.4.13
11+
- DJANGO=Django==1.5.8
12+
- DJANGO=Django==1.6.5
1413
- DJANGO=https://github.com/django/django/tarball/stable/1.7.x
1514

1615
install:
@@ -21,16 +20,16 @@ script: coverage run -a setup.py test
2120
matrix:
2221
exclude:
2322
- python: 2.6
24-
env: DJANGO=Django==1.6.2
23+
env: DJANGO=Django==1.6.5
2524
- python: 2.6
2625
env: DJANGO=https://github.com/django/django/tarball/stable/1.7.x
2726
- python: 3.2
28-
env: DJANGO=Django==1.3.7
27+
env: DJANGO=Django==1.4.13
2928
- python: 3.3
30-
env: DJANGO=Django==1.3.7
31-
- python: 3.2
32-
env: DJANGO=Django==1.4.10
33-
- python: 3.3
34-
env: DJANGO=Django==1.4.10
29+
env: DJANGO=Django==1.4.13
30+
31+
include:
32+
- python: 3.4
33+
env: DJANGO=https://github.com/django/django/tarball/stable/1.7.x
3534

3635
after_success: coveralls

docs/usage.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ Django tutorial:
4646
Now all changes to ``Poll`` and ``Choice`` model instances will be tracked in
4747
the database.
4848

49+
The historical models can also track who made each change. To populate
50+
the history user automatically you can add middleware to your Django
51+
settings:
52+
53+
.. code-block:: python
54+
55+
MIDDLEWARE_CLASSES = [
56+
# ...
57+
'simple_history.middleware.HistoryRequestMiddleware',
58+
]
59+
60+
If you do not want to use the middleware, you can explicitly indicate
61+
the user making the change as indicated in the advanced usage
62+
documentation.
63+
4964
.. _admin_integration:
5065

5166
Integration with Django Admin

simple_history/middleware.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from . models import HistoricalRecords
2+
3+
4+
class HistoryRequestMiddleware(object):
5+
"""Expose request to HistoricalRecords.
6+
7+
This middleware sets request as a local thread variable, making it
8+
available to the model-level utilities to allow tracking of the
9+
authenticated user making a change.
10+
"""
11+
12+
def process_request(self, request):
13+
HistoricalRecords.thread.request = request

simple_history/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import unicode_literals
22

3+
import threading
34
import copy
45
try:
56
from django.apps import apps # Django >= 1.7
@@ -53,6 +54,8 @@ def python_2_unicode_compatible(klass):
5354

5455

5556
class HistoricalRecords(object):
57+
thread = threading.local()
58+
5659
def __init__(self, verbose_name=None, bases=(models.Model,)):
5760
self.user_set_verbose_name = verbose_name
5861
try:
@@ -213,14 +216,24 @@ def post_delete(self, instance, **kwargs):
213216

214217
def create_historical_record(self, instance, type):
215218
history_date = getattr(instance, '_history_date', now())
216-
history_user = getattr(instance, '_history_user', None)
219+
history_user = self.get_history_user(instance)
217220
manager = getattr(instance, self.manager_name)
218221
attrs = {}
219222
for field in instance._meta.fields:
220223
attrs[field.attname] = getattr(instance, field.attname)
221224
manager.create(history_date=history_date, history_type=type,
222225
history_user=history_user, **attrs)
223226

227+
def get_history_user(self, instance):
228+
"""Get the modifying user from instance or middleware."""
229+
try:
230+
return instance._history_user
231+
except AttributeError:
232+
try:
233+
return self.thread.request.user
234+
except AttributeError:
235+
return
236+
224237

225238
class ForeignKeyMixin(object):
226239
def get_attname(self):

simple_history/tests/tests/test_admin.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime, timedelta
22
from django_webtest import WebTest
3+
from django.test.utils import override_settings
34
from django import VERSION
45
from django.core.urlresolvers import reverse
56
try:
@@ -8,6 +9,7 @@
89
except ImportError: # django 1.4 compatibility
910
from django.contrib.auth.models import User
1011
from django.contrib.admin.util import quote
12+
from django.conf import settings
1113

1214
from ..models import Book, Person, Poll
1315

@@ -156,3 +158,26 @@ def test_historical_user_with_setter(self):
156158
self.login()
157159
add_page = self.app.get(reverse('admin:tests_paper_add'))
158160
add_page.form.submit()
161+
162+
def test_history_user_not_saved(self):
163+
self.login()
164+
poll = Poll.objects.create(question="why?", pub_date=today)
165+
historical_poll = poll.history.all()[0]
166+
self.assertIsNone(
167+
historical_poll.history_user,
168+
"No way to know of request, history_user should be unset.",
169+
)
170+
171+
def test_middleware_saves_user(self):
172+
overridden_settings = {
173+
'MIDDLEWARE_CLASSES':
174+
settings.MIDDLEWARE_CLASSES
175+
+ ['simple_history.middleware.HistoryRequestMiddleware'],
176+
}
177+
with override_settings(**overridden_settings):
178+
self.login()
179+
poll = Poll.objects.create(question="why?", pub_date=today)
180+
historical_poll = poll.history.all()[0]
181+
self.assertEqual(historical_poll.history_user, self.user,
182+
"Middleware should make the request available to "
183+
"retrieve history_user.")

tox.ini

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
[tox]
22
envlist =
3-
py26-1.3, py26-1.4, py26-1.5, py26-1.6,
4-
py27-1.3, py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk,
3+
py26-1.4, py26-1.5, py26-1.6,
4+
py27-1.4, py27-1.5, py27-1.6, py27-1.7, py27-trunk,
55
py32-1.5, py32-1.6, py32-1.7, py32-trunk,
66
py33-1.5, py33-1.6, py33-1.7, py33-trunk,
7+
py34-1.7, py34-trunk,
78
docs, flake8
89

910

@@ -27,28 +28,22 @@ exclude = __init__.py
2728
deps = flake8
2829
commands = flake8 simple_history
2930

30-
[testenv:py26-1.3]
31-
basepython = python2.6
32-
deps =
33-
django == 1.3.7
34-
coverage == 3.6
35-
3631
[testenv:py26-1.4]
3732
basepython = python2.6
3833
deps =
39-
django == 1.4.10
34+
django == 1.4.13
4035
coverage == 3.6
4136

4237
[testenv:py26-1.5]
4338
basepython = python2.6
4439
deps =
45-
django == 1.5.5
40+
django == 1.5.8
4641
coverage == 3.6
4742

4843
[testenv:py26-1.6]
4944
basepython = python2.6
5045
deps =
51-
django == 1.6.2
46+
django == 1.6.5
5247
coverage == 3.6
5348

5449
[testenv:py26-trunk]
@@ -57,29 +52,22 @@ deps =
5752
https://github.com/django/django/tarball/master
5853
coverage == 3.6
5954

60-
61-
[testenv:py27-1.3]
62-
basepython = python2.7
63-
deps =
64-
django == 1.3.7
65-
coverage == 3.6
66-
6755
[testenv:py27-1.4]
6856
basepython = python2.7
6957
deps =
70-
django == 1.4.10
58+
django == 1.4.13
7159
coverage == 3.6
7260

7361
[testenv:py27-1.5]
7462
basepython = python2.7
7563
deps =
76-
django == 1.5.5
64+
django == 1.5.8
7765
coverage == 3.6
7866

7967
[testenv:py27-1.6]
8068
basepython = python2.7
8169
deps =
82-
django == 1.6.2
70+
django == 1.6.5
8371
coverage == 3.6
8472

8573
[testenv:py27-1.7]
@@ -98,13 +86,13 @@ deps =
9886
[testenv:py32-1.5]
9987
basepython = python3.2
10088
deps =
101-
django == 1.5.5
89+
django == 1.5.8
10290
coverage == 3.6
10391

10492
[testenv:py32-1.6]
10593
basepython = python3.2
10694
deps =
107-
django == 1.6.2
95+
django == 1.6.5
10896
coverage == 3.6
10997

11098
[testenv:py32-1.7]
@@ -123,13 +111,13 @@ deps =
123111
[testenv:py33-1.5]
124112
basepython = python3.3
125113
deps =
126-
django == 1.5.5
114+
django == 1.5.8
127115
coverage == 3.6
128116

129117
[testenv:py33-1.6]
130118
basepython = python3.3
131119
deps =
132-
django == 1.6.2
120+
django == 1.6.5
133121
coverage == 3.6
134122

135123
[testenv:py33-1.7]
@@ -143,3 +131,15 @@ basepython = python3.3
143131
deps =
144132
https://github.com/django/django/tarball/master
145133
coverage == 3.6
134+
135+
[testenv:py34-1.7]
136+
basepython = python3.4
137+
deps =
138+
https://github.com/django/django/tarball/stable/1.7.x
139+
coverage == 3.6
140+
141+
[testenv:py34-trunk]
142+
basepython = python3.4
143+
deps =
144+
https://github.com/django/django/tarball/master
145+
coverage == 3.6

0 commit comments

Comments
 (0)