Skip to content

Commit 3183611

Browse files
authored
Merge pull request #471 from kseever/422-add-history-extra-fields
Send history_instance in the `pre_create_historical_record` signal
2 parents a6fddd6 + 6606cfb commit 3183611

File tree

9 files changed

+204
-19
lines changed

9 files changed

+204
-19
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Unreleased
55
----------
66
- Add `custom_model_name` parameter to the constructor of `HistoricalRecords` (gh-451)
77
- Fix header on history pages when custom site_header is used (gh-448)
8+
- Modify `pre_create_historircal_record` to pass `history_instance` for ease of customization (gh-421)
89
- Raise warning if HistoricalRecords(inherit=False) is in an abstract model (gh-341)
910

1011
2.5.1 (2018-10-19)

docs/advanced.rst

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ folder of the package you register the model in, you could do:
7676
You can pass attributes of ``HistoricalRecords`` directly to ``register``:
7777

7878
.. code-block:: python
79-
79+
8080
register(User, excluded_fields=['last_login']))
8181
8282
For a complete list of the attributes you can pass to ``register`` we refer
@@ -361,6 +361,55 @@ And you don't want to store the changes for the field ``pub_date``, it is necess
361361
362362
By default, django-simple-history stores the changes for all fields in the model.
363363

364+
Adding additional fields to historical models
365+
---------------------------------------------
366+
367+
Sometimes it is useful to be able to add additional fields to historical models that do not exist on the
368+
source model. This is possible by combining the ``bases`` functionality with the ``pre_create_historical_record`` signal.
369+
370+
.. code-block:: python
371+
372+
# in models.py
373+
class IPAddressHistoricalModel(models.Model):
374+
"""
375+
Abstract model for history models tracking the IP address.
376+
"""
377+
ip_address = models.GenericIPAddressField(_('IP address'))
378+
379+
class Meta:
380+
abstract = True
381+
382+
383+
class PollWithExtraFields(models.Model):
384+
question = models.CharField(max_length=200)
385+
pub_date = models.DateTimeField('date published')
386+
387+
history = HistoricalRecords(bases=[IPAddressHistoricalModel,]
388+
389+
390+
.. code-block:: python
391+
392+
# define your signal handler/callback anywhere outside of models.py
393+
def add_history_ip_address(sender, **kwargs):
394+
history_instance = kwargs['history_instance']
395+
# thread.request for use only when the simple_history middleware is on and enabled
396+
history_instance.ip_address = HistoricalRecords.thread.request.META['REMOTE_ADDR']
397+
398+
399+
.. code-block:: python
400+
401+
# in apps.py
402+
class TestsConfig(AppConfig):
403+
def ready(self):
404+
from simple_history.tests.models \
405+
import HistoricalPollWithExtraFields
406+
407+
pre_create_historical_record.connect(
408+
add_history_ip_address,
409+
sender=HistoricalPollWithExtraFields
410+
)
411+
412+
364413
Change Reason
365414
-------------
366415
@@ -462,7 +511,20 @@ This may be useful when you want to construct timelines and need to get only the
462511
463512
Using signals
464513
------------------------------------
465-
django-simple-history includes signals that helps you provide custom behaviour when saving a historical record. If you want to connect the signals you can do so using the following code:
514+
`django-simple-history` includes signals that help you provide custom behavior when saving a historical record. Arguments passed to the signals include the following:
515+
516+
instance
517+
The source model instance being saved
518+
history_instance
519+
The corresponding history record
520+
history_date
521+
Datetime of the history record's creation
522+
history_change_reason
523+
Freetext description of the reason for the change
524+
history_user
525+
The user that instigated the change
526+
527+
To connect the signals to your callbacks, you can use the @receiver decorator:
466528
467529
.. code-block:: python
468530
@@ -473,9 +535,9 @@ django-simple-history includes signals that helps you provide custom behaviour w
473535
)
474536
475537
@receiver(pre_create_historical_record)
476-
def pre_create_historical_record(sender, instance, **kwargs):
538+
def pre_create_historical_record_callback(sender, **kwargs):
477539
print("Sent before saving historical record")
478540
479-
@receiver(pre_create_historical_record)
480-
def post_create_historical_record(sender, instance, history_instance, **kwargs):
481-
print("Sent after saving historical record")
541+
@receiver(post_create_historical_record)
542+
def post_create_historical_record_callback(sender, **kwargs):
543+
print("Sent after saving historical record")

simple_history/models.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -362,21 +362,26 @@ def create_historical_record(self, instance, history_type):
362362
history_change_reason = getattr(instance, 'changeReason', None)
363363
manager = getattr(instance, self.manager_name)
364364

365-
pre_create_historical_record.send(
366-
sender=manager.model, instance=instance,
367-
history_date=history_date, history_user=history_user,
368-
history_change_reason=history_change_reason,
369-
)
370-
371365
attrs = {}
372366
for field in self.fields_included(instance):
373367
attrs[field.attname] = getattr(instance, field.attname)
374-
history_instance = manager.create(
368+
369+
history_instance = manager.model(
375370
history_date=history_date, history_type=history_type,
376371
history_user=history_user,
377-
history_change_reason=history_change_reason, **attrs
372+
history_change_reason=history_change_reason,
373+
**attrs
378374
)
379375

376+
pre_create_historical_record.send(
377+
sender=manager.model, instance=instance,
378+
history_date=history_date, history_user=history_user,
379+
history_change_reason=history_change_reason,
380+
history_instance=history_instance,
381+
)
382+
383+
history_instance.save()
384+
380385
post_create_historical_record.send(
381386
sender=manager.model, instance=instance,
382387
history_instance=history_instance,

simple_history/signals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33

44
pre_create_historical_record = django.dispatch.Signal(providing_args=[
5-
'instance', 'history_date', 'history_user', 'history_change_reason',
5+
'instance', 'history_instance', 'history_date', 'history_user',
6+
'history_change_reason',
67
])
78
post_create_historical_record = django.dispatch.Signal(providing_args=[
89
'instance', 'history_instance', 'history_date', 'history_user',

simple_history/tests/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from django.apps import apps
66
from django.conf import settings
77
from django.db import models
8+
from django.dispatch import receiver
89
from django.urls import reverse
910

1011
from simple_history import register
1112
from simple_history.models import HistoricalRecords
13+
from simple_history.signals import pre_create_historical_record
1214
from .custom_user.models import CustomUser as User
1315
from .external.models.model1 import AbstractExternal
1416

@@ -40,6 +42,23 @@ class PollWithExcludedFKField(models.Model):
4042
history = HistoricalRecords(excluded_fields=['place'])
4143

4244

45+
class IPAddressHistoricalModel(models.Model):
46+
ip_address = models.GenericIPAddressField()
47+
48+
class Meta:
49+
abstract = True
50+
51+
52+
class PollWithHistoricalIPAddress(models.Model):
53+
question = models.CharField(max_length=200)
54+
pub_date = models.DateTimeField('date published')
55+
56+
history = HistoricalRecords(bases=[IPAddressHistoricalModel])
57+
58+
def get_absolute_url(self):
59+
return reverse('poll-detail', kwargs={'pk': self.pk})
60+
61+
4362
class Temperature(models.Model):
4463
location = models.CharField(max_length=200)
4564
temperature = models.IntegerField()

simple_history/tests/tests/test_models.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
from django.core.files.base import ContentFile
1111
from django.db import models
1212
from django.db.models.fields.proxy import OrderWrt
13-
from django.test import TestCase
13+
from django.test import override_settings, TestCase
14+
from django.urls import reverse
1415

1516
from simple_history.models import (
1617
HistoricalRecords,
1718
ModelChange
1819
)
20+
from simple_history.signals import pre_create_historical_record
21+
from simple_history.tests.tests.utils import middleware_override_settings
1922
from simple_history.utils import update_change_reason
2023
from ..external.models import ExternalModel2, ExternalModel4
2124
from ..models import (
@@ -43,6 +46,7 @@
4346
HistoricalChoice,
4447
HistoricalCustomFKError,
4548
HistoricalPoll,
49+
HistoricalPollWithHistoricalIPAddress,
4650
HistoricalState,
4751
Library,
4852
MultiOneToOne,
@@ -52,6 +56,7 @@
5256
PollInfo,
5357
PollWithExcludeFields,
5458
PollWithExcludedFKField,
59+
PollWithHistoricalIPAddress,
5560
Province,
5661
Restaurant,
5762
SelfFK,
@@ -1050,6 +1055,81 @@ def test_changed_value_lost(self):
10501055
self.assertEqual(instance.place, new_place)
10511056

10521057

1058+
def add_static_history_ip_address(sender, **kwargs):
1059+
history_instance = kwargs['history_instance']
1060+
history_instance.ip_address = '192.168.0.1'
1061+
1062+
1063+
class ExtraFieldsStaticIPAddressTestCase(TestCase):
1064+
def setUp(self):
1065+
pre_create_historical_record.connect(
1066+
add_static_history_ip_address,
1067+
sender=HistoricalPollWithHistoricalIPAddress,
1068+
dispatch_uid='add_static_history_ip_address'
1069+
)
1070+
1071+
def tearDown(self):
1072+
pre_create_historical_record.disconnect(
1073+
add_static_history_ip_address,
1074+
sender=HistoricalPollWithHistoricalIPAddress,
1075+
dispatch_uid='add_static_history_ip_address'
1076+
)
1077+
1078+
def test_extra_ip_address_field_populated_on_save(self):
1079+
poll = PollWithHistoricalIPAddress.objects.create(
1080+
question="Will it blend?", pub_date=today
1081+
)
1082+
1083+
poll_history = poll.history.first()
1084+
1085+
self.assertEquals('192.168.0.1', poll_history.ip_address)
1086+
1087+
def test_extra_ip_address_field_not_present_on_poll(self):
1088+
poll = PollWithHistoricalIPAddress.objects.create(
1089+
question="Will it blend?", pub_date=today
1090+
)
1091+
1092+
with self.assertRaises(AttributeError):
1093+
poll.ip_address
1094+
1095+
1096+
def add_dynamic_history_ip_address(sender, **kwargs):
1097+
history_instance = kwargs['history_instance']
1098+
history_instance.ip_address = \
1099+
HistoricalRecords.thread.request.META['REMOTE_ADDR']
1100+
1101+
1102+
@override_settings(**middleware_override_settings)
1103+
class ExtraFieldsDynamicIPAddressTestCase(TestCase):
1104+
def setUp(self):
1105+
pre_create_historical_record.connect(
1106+
add_dynamic_history_ip_address,
1107+
sender=HistoricalPollWithHistoricalIPAddress,
1108+
dispatch_uid='add_dynamic_history_ip_address'
1109+
)
1110+
1111+
def tearDown(self):
1112+
pre_create_historical_record.disconnect(
1113+
add_dynamic_history_ip_address,
1114+
sender=HistoricalPollWithHistoricalIPAddress,
1115+
dispatch_uid='add_dynamic_history_ip_address'
1116+
)
1117+
1118+
def test_signal_is_able_to_retrieve_request_from_thread(self):
1119+
data = {
1120+
'question': 'Will it blend?',
1121+
'pub_date': '2018-10-30'
1122+
}
1123+
1124+
self.client.post(reverse('pollip-add'), data=data)
1125+
1126+
polls = PollWithHistoricalIPAddress.objects.all()
1127+
self.assertEqual(1, polls.count())
1128+
1129+
poll_history = polls[0].history.first()
1130+
self.assertEqual('127.0.0.1', poll_history.ip_address)
1131+
1132+
10531133
class WarningOnAbstractModelWithInheritFalseTest(TestCase):
10541134
def test_warning_on_abstract_model_with_inherit_false(self):
10551135

simple_history/tests/tests/test_signals.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_pre_create_historical_record_signal(self):
2525
def handler(sender, instance, **kwargs):
2626
self.signal_was_called = True
2727
self.signal_instance = instance
28+
self.signal_history_instance = kwargs['history_instance']
2829
self.signal_sender = sender
2930
pre_create_historical_record.connect(handler)
3031

@@ -33,6 +34,7 @@ def handler(sender, instance, **kwargs):
3334

3435
self.assertTrue(self.signal_was_called)
3536
self.assertEqual(self.signal_instance, p)
37+
self.assertIsNotNone(self.signal_history_instance)
3638
self.assertEqual(self.signal_sender, p.history.first().__class__)
3739

3840
def test_post_create_historical_record_signal(self):
@@ -48,5 +50,5 @@ def handler(sender, instance, history_instance, **kwargs):
4850

4951
self.assertTrue(self.signal_was_called)
5052
self.assertEqual(self.signal_instance, p)
51-
self.assertTrue(self.signal_history_instance is not None)
53+
self.assertIsNotNone(self.signal_history_instance)
5254
self.assertEqual(self.signal_sender, p.history.first().__class__)

simple_history/tests/urls.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
PollDelete,
1111
PollDetail,
1212
PollList,
13-
PollUpdate
13+
PollUpdate,
14+
PollWithHistoricalIPAddressCreate,
1415
)
1516
from . import other_admin
1617

@@ -26,6 +27,11 @@
2627
BucketDataRegisterRequestUserDetail.as_view(),
2728
name='bucket_data-detail'),
2829
url(r'^poll/add/$', PollCreate.as_view(), name='poll-add'),
30+
url(
31+
r'^pollwithhistoricalipaddress/add$',
32+
PollWithHistoricalIPAddressCreate.as_view(),
33+
name='pollip-add'
34+
),
2935
url(r'^poll/(?P<pk>[0-9]+)/$', PollUpdate.as_view(), name='poll-update'),
3036
url(r'^poll/(?P<pk>[0-9]+)/delete/$', PollDelete.as_view(),
3137
name='poll-delete'),

simple_history/tests/view.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,23 @@
77
UpdateView
88
)
99

10-
from simple_history.tests.models import BucketDataRegisterRequestUser, Poll
10+
from simple_history.tests.models import (
11+
BucketDataRegisterRequestUser,
12+
Poll,
13+
PollWithHistoricalIPAddress
14+
)
1115

1216

1317
class PollCreate(CreateView):
1418
model = Poll
1519
fields = ['question', 'pub_date']
1620

1721

22+
class PollWithHistoricalIPAddressCreate(CreateView):
23+
model = PollWithHistoricalIPAddress
24+
fields = ['question', 'pub_date']
25+
26+
1827
class PollUpdate(UpdateView):
1928
model = Poll
2029
fields = ['question', 'pub_date']

0 commit comments

Comments
 (0)