Skip to content

Commit 46fe5f1

Browse files
committed
Send history_instance in the pre_create_historical_record signal
This allows for better customization of historical records without incurring extra database queries resulting from modifying the instance after save in `post_create_historical_record`.
1 parent 168eecc commit 46fe5f1

File tree

7 files changed

+145
-15
lines changed

7 files changed

+145
-15
lines changed

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+
instance = kwargs['instance']
395+
history_instance = kwargs['history_instance']
396+
history_instance.ip_address = instance.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 helps you provide custom behaviour 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
@@ -356,21 +356,26 @@ def create_historical_record(self, instance, history_type):
356356
history_change_reason = getattr(instance, 'changeReason', None)
357357
manager = getattr(instance, self.manager_name)
358358

359-
pre_create_historical_record.send(
360-
sender=manager.model, instance=instance,
361-
history_date=history_date, history_user=history_user,
362-
history_change_reason=history_change_reason,
363-
)
364-
365359
attrs = {}
366360
for field in self.fields_included(instance):
367361
attrs[field.attname] = getattr(instance, field.attname)
368-
history_instance = manager.create(
362+
363+
history_instance = manager.model(
369364
history_date=history_date, history_type=history_type,
370365
history_user=history_user,
371-
history_change_reason=history_change_reason, **attrs
366+
history_change_reason=history_change_reason,
367+
**attrs
372368
)
373369

370+
pre_create_historical_record.send(
371+
sender=manager.model, instance=instance,
372+
history_date=history_date, history_user=history_user,
373+
history_change_reason=history_change_reason,
374+
history_instance=history_instance,
375+
)
376+
377+
history_instance.save()
378+
374379
post_create_historical_record.send(
375380
sender=manager.model, instance=instance,
376381
history_instance=history_instance,

simple_history/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
default_app_config = 'simple_history.tests.apps.TestsConfig'

simple_history/tests/apps.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.apps import AppConfig
2+
from simple_history.signals import pre_create_historical_record
3+
4+
5+
def add_history_ip_address(sender, **kwargs):
6+
history_instance = kwargs['history_instance']
7+
history_instance.ip_address = '127.0.0.1'
8+
9+
10+
class TestsConfig(AppConfig):
11+
name = 'simple_history.tests'
12+
verbose_name = 'Tests'
13+
14+
def ready(self):
15+
from simple_history.tests.models \
16+
import HistoricalPollWithHistoricalIPAddress
17+
18+
pre_create_historical_record.connect(
19+
add_history_ip_address,
20+
sender=HistoricalPollWithHistoricalIPAddress
21+
)

simple_history/tests/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
from django.apps import apps
66
from django.conf import settings
7+
from django.dispatch import receiver
78
from django.db import models
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,21 @@ 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+
place = models.ForeignKey('Place', on_delete=models.CASCADE)
56+
57+
history = HistoricalRecords(bases=[IPAddressHistoricalModel])
58+
59+
4360
class Temperature(models.Model):
4461
location = models.CharField(max_length=200)
4562
temperature = models.IntegerField()

simple_history/tests/tests/test_models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
Place,
5151
Poll,
5252
PollInfo,
53+
PollWithHistoricalIPAddress,
5354
PollWithExcludeFields,
5455
PollWithExcludedFKField,
5556
Province,
@@ -1048,3 +1049,24 @@ def test_changed_value_lost(self):
10481049
historical = self.get_first_historical()
10491050
instance = historical.instance
10501051
self.assertEqual(instance.place, new_place)
1052+
1053+
1054+
class ExtraFieldsTestCase(TestCase):
1055+
def test_extra_ip_address_field_populated_on_save(self):
1056+
poll = PollWithHistoricalIPAddress.objects.create(
1057+
question="Will it blend?", pub_date=today,
1058+
place=Place.objects.create(name="Here")
1059+
)
1060+
1061+
poll_history = poll.history.order_by('history_date')[0]
1062+
1063+
self.assertEquals('127.0.0.1', poll_history.ip_address)
1064+
1065+
def test_extra_ip_address_field_not_present_on_poll(self):
1066+
poll = PollWithHistoricalIPAddress.objects.create(
1067+
question="Will it blend?", pub_date=today,
1068+
place=Place.objects.create(name="Here")
1069+
)
1070+
1071+
with self.assertRaises(AttributeError):
1072+
poll.ip_address

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__)

0 commit comments

Comments
 (0)