Skip to content

Commit 8d5809c

Browse files
only-entertainmentRoss Mechanic
authored andcommitted
Feature/issue 379 history change reason (#379)
* First pass at changing how the history_change_reason works * Use a better name for the class of the field. * Initial work with passing tests * Fix default field type Make max_length configurable via settings if the other field is configured via settings. * Add tests Fix setting name * Fix positional arguments suggestion, add my new thing at the end. * Again, consider the usage of positional args. Move history_id after excluded fields. * Trying to get my tests working. Revert null change on field * Still working on my tests. Error: Traceback (most recent call last): File "/Users/kneuharth/projects/django-simple-history/simple_history/tests/tests/test_models.py", line 402, in test_textfield_history_change_reason2 TextFieldChangeReasonModel2.objects.create(greeting="what's up?") File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/manager.py", line 82, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/query.py", line 417, in create obj.save(force_insert=True, using=self.db) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/base.py", line 729, in save force_update=force_update, update_fields=update_fields) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/base.py", line 769, in save_base update_fields=update_fields, raw=raw, using=using, File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/dispatch/dispatcher.py", line 178, in send for receiver in self._live_receivers(sender) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/dispatch/dispatcher.py", line 178, in <listcomp> for receiver in self._live_receivers(sender) File "/Users/kneuharth/projects/django-simple-history/simple_history/models.py", line 316, in post_save self.create_historical_record(instance, created and '+' or '~') File "/Users/kneuharth/projects/django-simple-history/simple_history/models.py", line 332, in create_historical_record history_change_reason=history_change_reason, **attrs) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/manager.py", line 82, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/query.py", line 415, in create obj = self.model(**kwargs) File "/Users/kneuharth/projects/django-simple-history/.eggs/Django-2.0.4-py3.5.egg/django/db/models/base.py", line 495, in __init__ raise TypeError("'%s' is an invalid keyword argument for this function" % kwarg) TypeError: 'history_change_reason' is an invalid keyword argument for this function * Fix indexing * Cleanup and passing tests now. Abandoned the max_length flag because TextField doesn’t use it. * Cleanup after pep8 and pyflakes and start of docs. * Finish the documentation for usage of customizations. * Sleight documentation tweaks.
1 parent 908a4fa commit 8d5809c

File tree

7 files changed

+157
-10
lines changed

7 files changed

+157
-10
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Authors
3232
- Jonathan Sanchez
3333
- Josh Fyne
3434
- Klaas van Schelven
35+
- Kris Neuharth
3536
- Maciej "RooTer" Urbański
3637
- Martin Bachwerk
3738
- Marty Alchin

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Changes
33

44
Unreleased
55
----------
6+
- Add ability to specify custom history_reason field (gh-379)
67
- Add ability to specify custom history_id field (gh-368)
78
- Add HistoricalRecord instance properties `prev_record` and `next_record` (gh-365)
89
- Can set admin methods as attributes on object history change list template (gh-390)

docs/usage.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,55 @@ admin class
167167
.. image:: screens/5_history_list_display.png
168168

169169

170+
Customizations
171+
----------------
172+
173+
UUID as `history_id`
174+
~~~~~~~~~~~~~~~~~~~~
175+
176+
The ``HistoricalRecords`` object can be customized to use an ``UUIDField`` instead
177+
of the default ``IntegerField`` as the object `history_id` either through
178+
Django settings or via the constructor on the model.
179+
180+
.. code-block:: python
181+
182+
SIMPLE_HISTORY_HISTORY_ID_USE_UUID = True
183+
184+
or
185+
186+
.. code-block:: python
187+
188+
class UUIDExample(models.Model):
189+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
190+
history = HistoricalRecords(
191+
history_id_field=models.UUIDField(default=uuid.uuid4)
192+
)
193+
194+
195+
TextField as `history_change_reason`
196+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
197+
198+
The ``HistoricalRecords`` object can be customized to accept a
199+
``TextField`` model field for saving the
200+
`history_change_reason` either through settings or via the constructor on the
201+
model. The common use case for this is for supporting larger model change
202+
histories to support changelog-like features.
203+
204+
.. code-block:: python
205+
206+
SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD=True
207+
208+
or
209+
210+
.. code-block:: python
211+
212+
class TextFieldExample(models.Model):
213+
greeting = models.CharField(max_length=100)
214+
history = HistoricalRecords(
215+
history_change_reason_field=models.TextField(null=True)
216+
)
217+
218+
170219
Querying history
171220
----------------
172221

simple_history/models.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ class HistoricalRecords(object):
2929

3030
def __init__(self, verbose_name=None, bases=(models.Model,),
3131
user_related_name='+', table_name=None, inherit=False,
32+
excluded_fields=None,
3233
history_id_field=None,
33-
excluded_fields=None):
34+
history_change_reason_field=None):
3435
self.user_set_verbose_name = verbose_name
3536
self.user_related_name = user_related_name
3637
self.table_name = table_name
3738
self.inherit = inherit
3839
self.history_id_field = history_id_field
40+
self.history_change_reason_field = history_change_reason_field
3941
if excluded_fields is None:
4042
excluded_fields = []
4143
self.excluded_fields = excluded_fields
@@ -250,11 +252,25 @@ def get_prev_record(self):
250252
else:
251253
history_id_field = models.AutoField(primary_key=True)
252254

255+
if self.history_change_reason_field:
256+
# User specific field from init
257+
history_change_reason_field = self.history_change_reason_field
258+
elif getattr(
259+
settings, 'SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD',
260+
False
261+
):
262+
# Use text field with no max length, not enforced by DB anyways
263+
history_change_reason_field = models.TextField(null=True)
264+
else:
265+
# Current default, with max length
266+
history_change_reason_field = models.CharField(
267+
max_length=100, null=True
268+
)
269+
253270
return {
254271
'history_id': history_id_field,
255272
'history_date': models.DateTimeField(),
256-
'history_change_reason': models.CharField(max_length=100,
257-
null=True),
273+
'history_change_reason': history_change_reason_field,
258274
'history_user': models.ForeignKey(
259275
user_model, null=True, related_name=self.user_related_name,
260276
on_delete=models.SET_NULL),
@@ -306,6 +322,7 @@ def create_historical_record(self, instance, history_type):
306322
history_date = getattr(instance, '_history_date', now())
307323
history_user = self.get_history_user(instance)
308324
history_change_reason = getattr(instance, 'changeReason', None)
325+
309326
manager = getattr(instance, self.manager_name)
310327
attrs = {}
311328
for field in self.fields_included(instance):

simple_history/registry_tests/tests.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ def test_tracked_abstract_base(self):
101101
for f in TrackedWithAbstractBase.history.model._meta.fields
102102
],
103103
[
104-
'id', 'history_id', 'history_date',
105-
'history_change_reason', 'history_user_id',
104+
'id', 'history_id',
105+
'history_change_reason', 'history_date', 'history_user_id',
106106
'history_type',
107107
],
108108
)
@@ -115,7 +115,7 @@ def test_tracked_concrete_base(self):
115115
],
116116
[
117117
'id', 'trackedconcretebase_ptr_id', 'history_id',
118-
'history_date', 'history_change_reason', 'history_user_id',
118+
'history_change_reason', 'history_date', 'history_user_id',
119119
'history_type',
120120
],
121121
)
@@ -131,7 +131,7 @@ def test_tracked_abstract_and_untracked_concrete_base(self):
131131
[f.attname for f in InheritTracking1.history.model._meta.fields],
132132
[
133133
'id', 'untrackedconcretebase_ptr_id', 'history_id',
134-
'history_date', 'history_change_reason',
134+
'history_change_reason', 'history_date',
135135
'history_user_id', 'history_type',
136136
],
137137
)
@@ -141,7 +141,7 @@ def test_indirect_tracked_abstract_base(self):
141141
[f.attname for f in InheritTracking2.history.model._meta.fields],
142142
[
143143
'id', 'baseinherittracking2_ptr_id', 'history_id',
144-
'history_date', 'history_change_reason',
144+
'history_change_reason', 'history_date',
145145
'history_user_id', 'history_type',
146146
],
147147
)
@@ -151,7 +151,7 @@ def test_indirect_tracked_concrete_base(self):
151151
[f.attname for f in InheritTracking3.history.model._meta.fields],
152152
[
153153
'id', 'baseinherittracking3_ptr_id', 'history_id',
154-
'history_date', 'history_change_reason',
154+
'history_change_reason', 'history_date',
155155
'history_user_id', 'history_type',
156156
],
157157
)

simple_history/tests/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,28 @@ class UUIDDefaultModel(models.Model):
433433

434434
# Clear the SIMPLE_HISTORY_HISTORY_ID_USE_UUID
435435
delattr(settings, 'SIMPLE_HISTORY_HISTORY_ID_USE_UUID')
436+
437+
438+
# Set the SIMPLE_HISTORY_HISTORY_CHANGE_REASON_FIELD
439+
setattr(settings, 'SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD', True)
440+
441+
442+
class TextFieldChangeReasonModel1(models.Model):
443+
greeting = models.CharField(max_length=100)
444+
history = HistoricalRecords()
445+
446+
447+
# Clear the SIMPLE_HISTORY_HISTORY_CHANGE_REASON_FIELD
448+
delattr(settings, 'SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD')
449+
450+
451+
class TextFieldChangeReasonModel2(models.Model):
452+
greeting = models.CharField(max_length=100)
453+
history = HistoricalRecords(
454+
history_change_reason_field=models.TextField(null=True)
455+
)
456+
457+
458+
class CharFieldChangeReasonModel(models.Model):
459+
greeting = models.CharField(max_length=100)
460+
history = HistoricalRecords()

simple_history/tests/tests/test_models.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@
5353
UnicodeVerboseName,
5454
UUIDModel,
5555
UUIDDefaultModel,
56-
WaterLevel
56+
WaterLevel,
57+
TextFieldChangeReasonModel1,
58+
TextFieldChangeReasonModel2,
59+
CharFieldChangeReasonModel,
5760
)
5861

5962
get_model = apps.get_model
@@ -382,6 +385,57 @@ def test_uuid_default_history_id(self):
382385
history = entry.history.all()[0]
383386
self.assertTrue(isinstance(history.history_id, uuid.UUID))
384387

388+
def test_default_history_change_reason(self):
389+
entry = CharFieldChangeReasonModel.objects.create(
390+
greeting="what's up?"
391+
)
392+
history = entry.history.get()
393+
394+
self.assertEqual(history.history_change_reason, None)
395+
396+
def test_charfield_history_change_reason(self):
397+
# Default CharField and length
398+
entry = CharFieldChangeReasonModel.objects.create(
399+
greeting="what's up?"
400+
)
401+
entry.greeting = "what is happening?"
402+
entry.save()
403+
update_change_reason(entry, 'Change greeting.')
404+
405+
history = entry.history.all()[0]
406+
field = history._meta.get_field('history_change_reason')
407+
408+
self.assertTrue(isinstance(field, models.CharField))
409+
self.assertTrue(field.max_length, 100)
410+
411+
def test_textfield_history_change_reason1(self):
412+
# TextField usage is determined by settings
413+
entry = TextFieldChangeReasonModel1.objects.create(
414+
greeting="what's up?"
415+
)
416+
entry.greeting = "what is happening?"
417+
entry.save()
418+
update_change_reason(entry, 'Change greeting.')
419+
420+
history = entry.history.all()[0]
421+
field = history._meta.get_field('history_change_reason')
422+
423+
self.assertTrue(isinstance(field, models.TextField))
424+
425+
def test_textfield_history_change_reason2(self):
426+
# TextField instance is passed in init
427+
entry = TextFieldChangeReasonModel2.objects.create(
428+
greeting="what's up?"
429+
)
430+
entry.greeting = "what is happening?"
431+
entry.save()
432+
update_change_reason(entry, 'Change greeting.')
433+
434+
history = entry.history.all()[0]
435+
field = history._meta.get_field('history_change_reason')
436+
437+
self.assertTrue(isinstance(field, models.TextField))
438+
385439
def test_get_prev_record(self):
386440
poll = Poll(question="what's up?", pub_date=today)
387441
poll.save()

0 commit comments

Comments
 (0)