• ru
  • Language: en

Historical Model Customizations

Custom history_id

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 history_id``s 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.

Custom history_date

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

Custom history table name

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

Custom model name

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.

TextField as history_change_reason

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

Change Base Class of HistoricalRecord Models

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

Excluded Fields

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.

Adding additional fields to historical models

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

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, is possible to pass the _change_reason explicitly. For this, after a save or delete in an instance, is necessary 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')

Deleting historical record

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)

Allow tracking to be inherited

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.

History Model In Different App

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

FileField as a CharField

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