Skip to content

Commit c7fdfec

Browse files
authored
Merge pull request #275 from joaojunior/change_reason
add history change reason to allow reasoning for the changes[2]
2 parents 7ab7f95 + 6d282a7 commit c7fdfec

File tree

7 files changed

+130
-38
lines changed

7 files changed

+130
-38
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ tip (unreleased)
5151
- Fix a bug when models have a ``ForeignKey`` with ``primary_key=True``
5252
- Do NOT delete the history elements when a user is deleted.
5353
- Add support for ``latest``
54+
- Allow setting a reason for change. [using option changeReason]
5455

5556
1.5.3 (2014-11-18)
5657
------------------

docs/advanced.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,43 @@ You can use the ``table_name`` parameter with both ``HistoricalRecords()`` or
213213
pub_date = models.DateTimeField('date published')
214214
215215
register(Question, table_name='polls_question_history')
216+
217+
Change Reason
218+
-------------
219+
220+
Change reason is a message to explain why the change was made in the instance. It is stored in the
221+
field ``history_change_reason`` and its default value is ``None``.
222+
223+
By default, the django-simple-history gets the change reason in the field ``changeReason`` of the instance. Also, is possible to pass
224+
the ``changeReason`` explicitly. For this, after a save or delete in an instance, is necessary call the
225+
function ``utils.update_change_reason``. The first argument of this function is the instance and the seccond
226+
is the message that represents the change reason.
227+
228+
For instance, for the model:
229+
230+
.. code-block:: python
231+
232+
from django.db import models
233+
from simple_history.models import HistoricalRecords
234+
235+
class Poll(models.Model):
236+
question = models.CharField(max_length=200)
237+
history = HistoricalRecords()
238+
239+
You can create a instance with a implicity change reason.
240+
241+
.. code-block:: python
242+
243+
poll = Poll(question='Question 1')
244+
poll.changeReason = 'Add a question'
245+
poll.save()
246+
247+
Or you can pass the change reason explicitly:
248+
249+
.. code-block:: python
250+
251+
from simple_history.utils import update_change_reason
252+
253+
poll = Poll(question='Question 1')
254+
poll.save()
255+
update_change_reason(poll, 'Add a question')

simple_history/models.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
import importlib
55
import threading
66

7-
from django.db import models, router
8-
from django.db.models.fields.proxy import OrderWrt
97
from django.conf import settings
108
from django.contrib import admin
9+
from django.db import models, router
10+
from django.db.models.fields.proxy import OrderWrt
1111
from django.utils import six
12-
from django.utils.encoding import python_2_unicode_compatible
13-
from django.utils.encoding import smart_text
12+
from django.utils.encoding import python_2_unicode_compatible, smart_text
1413
from django.utils.timezone import now
1514
from django.utils.translation import string_concat, ugettext as _
1615

16+
from . import exceptions
17+
from .manager import HistoryDescriptor
18+
1719
try:
1820
from django.apps import apps
1921
except ImportError: # Django < 1.7
@@ -26,8 +28,6 @@
2628
add_introspection_rules(
2729
[], ["^simple_history.models.CustomForeignKeyField"])
2830

29-
from . import exceptions
30-
from .manager import HistoryDescriptor
3131

3232
registered_models = {}
3333

@@ -202,6 +202,8 @@ def get_instance(self):
202202
return {
203203
'history_id': models.AutoField(primary_key=True),
204204
'history_date': models.DateTimeField(),
205+
'history_change_reason': models.CharField(max_length=100,
206+
null=True),
205207
'history_user': models.ForeignKey(
206208
user_model, null=True, related_name=self.user_related_name,
207209
on_delete=models.SET_NULL),
@@ -247,12 +249,14 @@ def post_delete(self, instance, **kwargs):
247249
def create_historical_record(self, instance, history_type):
248250
history_date = getattr(instance, '_history_date', now())
249251
history_user = self.get_history_user(instance)
252+
history_change_reason = getattr(instance, 'changeReason', None)
250253
manager = getattr(instance, self.manager_name)
251254
attrs = {}
252255
for field in instance._meta.fields:
253256
attrs[field.attname] = getattr(instance, field.attname)
254257
manager.create(history_date=history_date, history_type=history_type,
255-
history_user=history_user, **attrs)
258+
history_user=history_user,
259+
history_change_reason=history_change_reason, **attrs)
256260

257261
def get_history_user(self, instance):
258262
"""Get the modifying user from instance or middleware."""

simple_history/registry_tests/migration_test_app/migrations/0001_initial.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Migration(migrations.Migration):
2929
('history_id', models.AutoField(primary_key=True, serialize=False)),
3030
('history_date', models.DateTimeField()),
3131
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
32+
('history_change_reason', models.CharField(max_length=100, null=True)),
3233
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
3334
],
3435
options={

simple_history/registry_tests/tests.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
from __future__ import unicode_literals
22

3-
from datetime import datetime, timedelta
4-
from six.moves import cStringIO as StringIO
53
import unittest
4+
from datetime import datetime, timedelta
65

76
import django
87
from django.contrib.auth import get_user_model
98
from django.core import management
109
from django.test import TestCase
11-
1210
from simple_history import exceptions, register
13-
from ..tests.models import (
14-
Poll, Choice, Voter, Restaurant, HistoricalPoll, HistoricalChoice,
15-
HistoricalState, HistoricalCustomFKError,
16-
UserAccessorDefault, UserAccessorOverride,
17-
TrackedAbstractBaseA, TrackedAbstractBaseB,
18-
TrackedWithAbstractBase, TrackedWithConcreteBase,
19-
InheritTracking1, InheritTracking2, InheritTracking3, InheritTracking4,
20-
)
11+
from six.moves import cStringIO as StringIO
12+
13+
from ..tests.models import (Choice, InheritTracking1, InheritTracking2,
14+
InheritTracking3, InheritTracking4, Poll,
15+
Restaurant, TrackedAbstractBaseA,
16+
TrackedAbstractBaseB, TrackedWithAbstractBase,
17+
TrackedWithConcreteBase, UserAccessorDefault,
18+
UserAccessorOverride, Voter)
2119

2220
try:
2321
from django.apps import apps
@@ -86,7 +84,8 @@ def test_tracked_abstract_base(self):
8684
for f in TrackedWithAbstractBase.history.model._meta.fields
8785
],
8886
[
89-
'id', 'history_id', 'history_date', 'history_user_id',
87+
'id', 'history_id', 'history_date',
88+
'history_change_reason', 'history_user_id',
9089
'history_type',
9190
],
9291
)
@@ -99,7 +98,8 @@ def test_tracked_concrete_base(self):
9998
],
10099
[
101100
'id', 'trackedconcretebase_ptr_id', 'history_id',
102-
'history_date', 'history_user_id', 'history_type',
101+
'history_date', 'history_change_reason', 'history_user_id',
102+
'history_type',
103103
],
104104
)
105105

@@ -114,7 +114,8 @@ def test_tracked_abstract_and_untracked_concrete_base(self):
114114
[f.attname for f in InheritTracking1.history.model._meta.fields],
115115
[
116116
'id', 'untrackedconcretebase_ptr_id', 'history_id',
117-
'history_date', 'history_user_id', 'history_type',
117+
'history_date', 'history_change_reason',
118+
'history_user_id', 'history_type',
118119
],
119120
)
120121

@@ -123,7 +124,8 @@ def test_indirect_tracked_abstract_base(self):
123124
[f.attname for f in InheritTracking2.history.model._meta.fields],
124125
[
125126
'id', 'baseinherittracking2_ptr_id', 'history_id',
126-
'history_date', 'history_user_id', 'history_type',
127+
'history_date', 'history_change_reason',
128+
'history_user_id', 'history_type',
127129
],
128130
)
129131

@@ -132,7 +134,8 @@ def test_indirect_tracked_concrete_base(self):
132134
[f.attname for f in InheritTracking3.history.model._meta.fields],
133135
[
134136
'id', 'baseinherittracking3_ptr_id', 'history_id',
135-
'history_date', 'history_user_id', 'history_type',
137+
'history_date', 'history_change_reason',
138+
'history_user_id', 'history_type',
136139
],
137140
)
138141

simple_history/tests/tests/test_models.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
from __future__ import unicode_literals
22

3-
from datetime import datetime, timedelta
43
import unittest
54
import warnings
5+
from datetime import datetime, timedelta
66

77
import django
88
from django.contrib.auth import get_user_model
99
from django.core.files.base import ContentFile
1010
from django.db import models
1111
from django.db.models.fields.proxy import OrderWrt
1212
from django.test import TestCase
13-
1413
from simple_history.models import HistoricalRecords, convert_auto_field
15-
from ..models import (
16-
AdminProfile, Bookcase, MultiOneToOne, Poll, Choice, Restaurant,
17-
Person, FileModel, Document, Book, HistoricalPoll, Library, State,
18-
AbstractBase, ConcreteAttr, ConcreteUtil, SelfFK, Temperature, WaterLevel,
19-
ExternalModel1, ExternalModel3, UnicodeVerboseName, HistoricalChoice,
20-
HistoricalState, HistoricalCustomFKError, Series, SeriesWork, PollInfo,
21-
Employee, Country, Province,
22-
City, Contact, ContactRegister,
23-
)
14+
from simple_history.utils import update_change_reason
15+
2416
from ..external.models import ExternalModel2, ExternalModel4
17+
from ..models import (AbstractBase, AdminProfile, Book, Bookcase, Choice, City,
18+
ConcreteAttr, ConcreteUtil, Contact, ContactRegister,
19+
Country, Document, Employee, ExternalModel1,
20+
ExternalModel3, FileModel, HistoricalChoice,
21+
HistoricalCustomFKError, HistoricalPoll, HistoricalState,
22+
Library, MultiOneToOne, Person, Poll, PollInfo, Province,
23+
Restaurant, SelfFK, Series, SeriesWork, State,
24+
Temperature, UnicodeVerboseName, WaterLevel)
2525

2626
try:
2727
from django.apps import apps
@@ -52,7 +52,7 @@ def assertRecordValues(self, record, klass, values_dict):
5252
self.assertEqual(getattr(record, key), value)
5353
self.assertEqual(record.history_object.__class__, klass)
5454
for key, value in values_dict.items():
55-
if key != 'history_type':
55+
if key not in ['history_type', 'history_change_reason']:
5656
self.assertEqual(getattr(record.history_object, key), value)
5757

5858
def test_create(self):
@@ -72,36 +72,63 @@ def test_update(self):
7272
p = Poll.objects.get()
7373
p.pub_date = tomorrow
7474
p.save()
75+
update_change_reason(p, 'future poll')
7576
update_record, create_record = p.history.all()
7677
self.assertRecordValues(create_record, Poll, {
7778
'question': "what's up?",
7879
'pub_date': today,
7980
'id': p.id,
81+
'history_change_reason': None,
8082
'history_type': "+"
8183
})
8284
self.assertRecordValues(update_record, Poll, {
8385
'question': "what's up?",
8486
'pub_date': tomorrow,
8587
'id': p.id,
88+
'history_change_reason': 'future poll',
8689
'history_type': "~"
8790
})
8891
self.assertDatetimesEqual(update_record.history_date, datetime.now())
8992

90-
def test_delete(self):
93+
def test_delete_verify_change_reason_implicitly(self):
94+
p = Poll.objects.create(question="what's up?", pub_date=today)
95+
poll_id = p.id
96+
p.changeReason = 'wrongEntry'
97+
p.delete()
98+
delete_record, create_record = Poll.history.all()
99+
self.assertRecordValues(create_record, Poll, {
100+
'question': "what's up?",
101+
'pub_date': today,
102+
'id': poll_id,
103+
'history_change_reason': None,
104+
'history_type': "+"
105+
})
106+
self.assertRecordValues(delete_record, Poll, {
107+
'question': "what's up?",
108+
'pub_date': today,
109+
'id': poll_id,
110+
'history_change_reason': 'wrongEntry',
111+
'history_type': "-"
112+
})
113+
114+
def test_delete_verify_change_reason_explicity(self):
91115
p = Poll.objects.create(question="what's up?", pub_date=today)
92116
poll_id = p.id
93117
p.delete()
118+
update_change_reason(p, 'wrongEntry')
94119
delete_record, create_record = Poll.history.all()
95120
self.assertRecordValues(create_record, Poll, {
96121
'question': "what's up?",
97122
'pub_date': today,
98123
'id': poll_id,
124+
'history_change_reason': None,
99125
'history_type': "+"
100126
})
101127
self.assertRecordValues(delete_record, Poll, {
102128
'question': "what's up?",
103129
'pub_date': today,
104130
'id': poll_id,
131+
'history_change_reason': 'wrongEntry',
105132
'history_type': "-"
106133
})
107134

@@ -492,7 +519,8 @@ def test_string_related(self):
492519
related_model = field_object.related.model
493520
self.assertEqual(related_model, HistoricalState)
494521

495-
@unittest.skipUnless(django.get_version() >= "1.7", "Requires 1.7 migrations")
522+
@unittest.skipUnless(django.get_version() >= "1.7",
523+
"Requires 1.7 migrations")
496524
def test_state_serialization_of_customfk(self):
497525
from django.db.migrations import state
498526
state.ModelState.from_model(HistoricalCustomFKError)
@@ -644,7 +672,8 @@ def test_restore_object_with_changed_order(self):
644672
self.assertEqual(order[5], self.w_chair.pk)
645673
self.assertEqual(order[6], self.w_battle.pk)
646674

647-
@unittest.skipUnless(django.get_version() >= "1.7", "Requires 1.7 migrations")
675+
@unittest.skipUnless(django.get_version() >= "1.7",
676+
"Requires 1.7 migrations")
648677
def test_migrations_include_order(self):
649678
from django.db.migrations import state
650679
model_state = state.ModelState.from_model(SeriesWork.history.model)

simple_history/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
def update_change_reason(instance, reason):
2+
attrs = {}
3+
model = type(instance)
4+
manager = instance if instance.id is not None else model
5+
for field in instance._meta.fields:
6+
value = getattr(instance, field.attname)
7+
if field.primary_key is True:
8+
if value is not None:
9+
attrs[field.attname] = value
10+
else:
11+
attrs[field.attname] = value
12+
record = manager.history.filter(**attrs).order_by('-history_date').first()
13+
record.history_change_reason = reason
14+
record.save()

0 commit comments

Comments
 (0)