By default, the historical table of a model will use an AutoField
for the table's history_id
(the history table's primary key). However, you can specify a different type of field for history_id
by passing a different field to history_id_field
parameter.
The example below uses a UUIDField
instead of an AutoField
:
import uuid
from django.db import models
from simple_history.models import HistoricalRecords
class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
history = HistoricalRecords(
history_id_field=models.UUIDField(default=uuid.uuid4)
)
Since using a UUIDField
for the history_id
is a common use case, there is a SIMPLE_HISTORY_HISTORY_ID_USE_UUID
setting that will set all instances of history_id
to UUIDs. Set this with the following line in your settings.py
file:
SIMPLE_HISTORY_HISTORY_ID_USE_UUID = True
This setting can still be overridden using the history_id_field
parameter on a per model basis.
You can use the history_id_field
parameter with both HistoricalRecords()
or register()
to change this behavior.
Note: regardless of what field type you specify as your history_id field, that field will automatically set primary_key=True
and editable=False
.
You're able to set a custom history_date
attribute for the historical record, by defining the property _history_date
in your model. That's helpful if you want to add versions to your model, which happened before the current model version, e.g. when batch importing historical data. The content of the property _history_date
has to be a datetime
-object, but setting the value of the property to a DateTimeField
, which is already defined in the model, will work too.
from django.db import models
from simple_history.models import HistoricalRecords
class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
changed_by = models.ForeignKey('auth.User')
history = HistoricalRecords()
__history_date = None
@property
def _history_date(self):
return self.__history_date
@_history_date.setter
def _history_date(self, value):
self.__history_date = value
from datetime import datetime
from models import Poll
my_poll = Poll(question="what's up?")
my_poll._history_date = datetime.now()
my_poll.save()
Many queries use history_date
as a filter. The as_of queries combine this with the original model's primary key to extract point-in-time snapshots of history. By default the history_date
field is indexed. You can control this behavior using settings.py.
# disable indexing on history_date
SIMPLE_HISTORY_DATE_INDEX = False
# enable indexing on history_date (default setting)
SIMPLE_HISTORY_DATE_INDEX = True
# enable composite indexing on history_date and model pk (to improve as_of queries)
# the string is case-insensitive
SIMPLE_HISTORY_DATE_INDEX = "Composite"
By default, the table name for historical models follow the Django convention and just add historical
before model name. For instance, if your application name is polls
and your model name Question
, then the table name will be polls_historicalquestion
.
You can use the table_name
parameter with both HistoricalRecords()
or register()
to change this behavior.
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
history = HistoricalRecords(table_name='polls_question_history')
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
register(Question, table_name='polls_question_history')
By default, historical model is named as 'Historical' + model name. For example, historical records for Choice
is called HistoricalChoice
. Users can specify a custom model name via the constructor on HistoricalRecords
. The common use case for this is avoiding naming conflict if the user already defined a model named as 'Historical' + model name.
This feature provides the ability to override the default model name used for the generated history model.
To configure history models to use a different name for the history model class, use an option named custom_model_name
. The value for this option can be a string or a callable. A simple string replaces the default name of 'Historical' + model name with the defined string. The most simple use case is illustrated below using a simple string:
class ModelNameExample(models.Model):
history = HistoricalRecords(
custom_model_name='SimpleHistoricalModelNameExample'
)
If you are using a base class for your models and want to apply a name change for the historical model for all models using the base class then a callable can be used. The callable is passed the name of the model for which the history model will be created. As an example using the callable mechanism, the below changes the default prefix Historical to `Audit`:
class Poll(models.Model):
question = models.CharField(max_length=200)
history = HistoricalRecords(custom_model_name=lambda x:f'Audit{x}')
class Opinion(models.Model):
opinion = models.CharField(max_length=2000)
register(Opinion, custom_model_name=lambda x:f'Audit{x}')
The resulting history class names would be AuditPoll and AuditOpinion. If the app the models are defined in is yoda then the corresponding history table names would be yoda_auditpoll and yoda_auditopinion
- IMPORTANT: Setting custom_model_name to lambda x:f'{x}' is not permitted.
An error will be generated and no history model created if they are the same.
The HistoricalRecords
object can be customized to accept a TextField
model field for saving the history_change_reason either through settings or via the constructor on the model. The common use case for this is for supporting larger model change histories to support changelog-like features.
SIMPLE_HISTORY_HISTORY_CHANGE_REASON_USE_TEXT_FIELD=True
or
class TextFieldExample(models.Model):
greeting = models.CharField(max_length=100)
history = HistoricalRecords(
history_change_reason_field=models.TextField(null=True)
)
To change the auto-generated HistoricalRecord models base class from models.Model
, pass in the abstract class in a list to bases
.
class RoutableModel(models.Model):
class Meta:
abstract = True
class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
changed_by = models.ForeignKey('auth.User')
history = HistoricalRecords(bases=[RoutableModel])
It is possible to use the parameter excluded_fields
to choose which fields will be stored on every create/update/delete.
For example, if you have the model:
class PollWithExcludeFields(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
And you don't want to store the changes for the field pub_date
, it is necessary to update the model to:
class PollWithExcludeFields(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
history = HistoricalRecords(excluded_fields=['pub_date'])
By default, django-simple-history stores the changes for all fields in the model.
Sometimes it is useful to be able to add additional fields to historical models that do not exist on the source model. This is possible by combining the bases
functionality with the pre_create_historical_record
signal.
# in models.py
class IPAddressHistoricalModel(models.Model):
"""
Abstract model for history models tracking the IP address.
"""
ip_address = models.GenericIPAddressField(_('IP address'))
class Meta:
abstract = True
class PollWithExtraFields(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
history = HistoricalRecords(bases=[IPAddressHistoricalModel,])
# define your signal handler/callback anywhere outside of models.py
def add_history_ip_address(sender, **kwargs):
history_instance = kwargs['history_instance']
# context.request for use only when the simple_history middleware is on and enabled
history_instance.ip_address = HistoricalRecords.context.request.META['REMOTE_ADDR']
# in apps.py
class TestsConfig(AppConfig):
def ready(self):
from simple_history.tests.models \
import HistoricalPollWithExtraFields
pre_create_historical_record.connect(
add_history_ip_address,
sender=HistoricalPollWithExtraFields
)
More information on signals in django-simple-history
is available in /signals
.
Change reason is a message to explain why the change was made in the instance. It is stored in the field history_change_reason
and its default value is None
.
By default, the django-simple-history gets the change reason in the field _change_reason
of the instance. Also, it is possible to pass the _change_reason
explicitly. For this, after a save or delete in an instance, it is necessary to call the function utils.update_change_reason
. The first argument of this function is the instance and the second is the message that represents the change reason.
For instance, for the model:
from django.db import models
from simple_history.models import HistoricalRecords
class Poll(models.Model):
question = models.CharField(max_length=200)
history = HistoricalRecords()
You can create an instance with an implicit change reason.
poll = Poll(question='Question 1')
poll._change_reason = 'Add a question'
poll.save()
Or you can pass the change reason explicitly:
from simple_history.utils import update_change_reason
poll = Poll(question='Question 1')
poll.save()
update_change_reason(poll, 'Add a question')
In some circumstances you may want to delete all the historical records when the master record is deleted. This can be accomplished by setting cascade_delete_history=True
.
class Poll(models.Model):
question = models.CharField(max_length=200)
history = HistoricalRecords(cascade_delete_history=True)
By default history tracking is only added for the model that is passed to register()
or has the HistoricalRecords
descriptor. By passing inherit=True
to either way of registering you can change that behavior so that any child model inheriting from it will have historical tracking as well. Be careful though, in cases where a model can be tracked more than once, MultipleRegistrationsError
will be raised.
from django.contrib.auth.models import User
from django.db import models
from simple_history import register
from simple_history.models import HistoricalRecords
# register() example
register(User, inherit=True)
# HistoricalRecords example
class Poll(models.Model):
history = HistoricalRecords(inherit=True)
Both User
and Poll
in the example above will cause any model inheriting from them to have historical tracking as well.
By default the app_label for the history model is the same as the base model. In some circumstances you may want to have the history models belong in a different app. This will support creating history models in a different database to the base model using database routing functionality based on app_label. To configure history models in a different app, add this to the HistoricalRecords instantiation or the record invocation: app="SomeAppName"
.
class Poll(models.Model):
question = models.CharField(max_length=200)
history = HistoricalRecords(app="SomeAppName")
class Opinion(models.Model):
opinion = models.CharField(max_length=2000)
register(Opinion, app="SomeAppName")
By default a FileField
in the base model becomes a TextField
in the history model. This is a historical choice that django-simple-history preserves for backwards compatibility; it is more correct for a FileField
to be converted to a CharField
instead. To opt into the new behavior, set the following line in your settings.py
file:
SIMPLE_HISTORY_FILEFIELD_TO_CHARFIELD = True
It is possible to use the parameter no_db_index
to choose which fields that will not create a database index.
For example, if you have the model:
class PollWithExcludeFields(models.Model):
question = models.CharField(max_length=200, db_index=True)
And you don't want to create database index for question
, it is necessary to update the model to:
class PollWithExcludeFields(models.Model):
question = models.CharField(max_length=200, db_index=True)
history = HistoricalRecords(no_db_index=['question'])
By default, django-simple-history keeps all indices. and even forces them on unique fields and relations. WARNING: This will drop performance on historical lookups
By default, many to many fields are ignored when tracking changes. If you want to track many to many relationships, you need to define them explicitly:
class Category(models.Model):
pass
class Poll(models.Model):
question = models.CharField(max_length=200)
categories = models.ManyToManyField(Category)
history = HistoricalRecords(many_to_many=[categories])
This will create a historical intermediate model that tracks each relational change between Poll and Category.
You will see the many to many changes when diffing between two historical records:
informal = Category(name="informal questions")
official = Category(name="official questions")
p = Poll.objects.create(question="what's up?")
p.save()
p.categories.add(informal, official)
p.categories.remove(informal)
last_record = p.history.latest()
previous_record = last_record.prev_record()
delta = last_record.diff_against(previous_record)
for change in delta.changes:
print("{} changed from {} to {}")
# Output:
# categories changed from [{'poll': 1, 'category': 1}, { 'poll': 1, 'category': 2}] to [{'poll': 1, 'category': 2}]