Package: django-wicked-historian
Django Wicked HistorianA complete solution for creating automatic history of Django models.InstallationPackage can be installed using PyPi:$ pip install django-wicked-historianYou can also use extras to ensure some additional dependencies specific for implementation of JSONField which is required for this package to work properly.$ pip install django-wicked-historian[mysql]
$ pip install django-wicked-historian[postgres]
$ pip install django-wicked-historian[django-jsonfield]Defining JSONField to be usedPackage requires some configuration. You need to specify JSONField implementation which package gonna use to store values of model fields in your settings:from wicked_historian.encoder import JSONEncoder

WICKED_HISTORIAN_JSON_FIELD_CLASS = 'path.to.JSONField'
WICKED_HISTORIAN_JSON_FIELD_KWARGS = {
    'encoder': JSONEncoder,
}WICKED_HISTORIAN_JSON_FIELD_CLASS- path to JSON field class to be usedWICKED_HISTORIAN_JSON_FIELD_KWARGS- kwargs used for instantiate of supplied classRemember to always usewicked_historian.encoder.JSONEncoderas an encoder for this field.Example configuration for common used fieldsjsonfield.JSONFieldWICKED_HISTORIAN_JSON_FIELD_CLASS = 'jsonfield.JSONField'
WICKED_HISTORIAN_JSON_FIELD_KWARGS = {
    'encoder_class': JSONEncoder,
}django.contrib.postgres.fields.JSONFieldFor Django >= 2.1 use builtin field with our encoder.WICKED_HISTORIAN_JSON_FIELD_CLASS = 'django.contrib.postgres.fields.JSONField'
WICKED_HISTORIAN_JSON_FIELD_KWARGS = {
    'encoder': JSONEncoder,
}django_mysql.models.fields.JSONFieldThis field in version 2.2.0 of django-mysql package doesn't support supplying custom json encoder. However this package supplies subclass of this field with support of custom encoders andwicked_historian.encoder.JSONEncoderis default encoder.Use fieldwicked_historian.compat.mysql.JSONFieldinstead:WICKED_HISTORIAN_JSON_FIELD_CLASS = 'wicked_historian.compat.mysql.JSONField'
WICKED_HISTORIAN_JSON_FIELD_KWARGS = {}Adding history to model of choiceModel for which history is going to be generated should inherit fromwicked_historian.models.DiffableHistoryModeland have a class for history entries specified in the Model.Meta class. History class should be created using factorywicked_historian.utils.generate_history_class:from wicked_historian.models import DiffableHistoryModel
from wicked_historian.utils import generate_history_class


class Book(DiffableHistoryModel):
    title = models.CharField(max_length=100)

    class Meta:
        history_class = 'this_app.models.BookEditHistory'


BookEditHistory = generate_history_class(Book, __name__)If there is a need for customizing the history model, it can be generated with theabstractoption and used as a base model for a custom history model.class BookEditHistory(generate_history_class(Book, __name__, abstract=True)):
    custom_field = models.IntegerField(default=0)

    def custom_method(self):
        return self.custom_field + 10Changes in model's fields setIf the set of model fields changes in a non-incremental way (fields were removed or changed their type), old definitions of such fields should be supplied to thegenerate_history_classfactory for handling already existing history entries concerning these fields:from wicked_historian.utils import ObsoleteFieldDescription

BookEditHistory = generate_history_class(
    Book,
    __name__,
    obsolete_field_choices=[
        ObsoleteFieldDescription('title', models.TextField()),
        ObsoleteFieldDescription('number_of_pages', models.IntegerField()),
        ObsoleteFieldDescription('age', models.IntegerField(choices=[
            (1, 'XV'),
            (2, 'XIX'),
            (3, 'XX'),
        ])),
    ],
)Excluding fields from historyIf there is no need for generating history for some fields, they can be excluded by supplying list of unwanted fields name togenerate_history_class. History for these will not be generated, but any already existing history can be read.from wicked_historian.utils import ObsoleteFieldDescription

BookEditHistory = generate_history_class(
    Book,
    __name__,
    excluded_fields=['title'],
)Choices in model fieldsPlease note that when the set of choices in model fields changes in a non-incremental way, some values may be impossible to restore from history entries. That's why you should always have a superset of all choices ever used in this fields declared in the field.Reading/accessing historyInstance history should be accessed by history model.Retrieving whole historyTo retrieve whole history use methodget_for, e.g.BookEditHistory.get_for(book)will return list of wholeBookinstance history as dicts.Filtering and searching historyTo filter history use history model manager (e.g.BookEditHistory.objects.filter(user=some_user, model=book)) and transform history entry to dict form usingget_entry_formethod.history_entry_instances = BookEditHistory.objects.filter(user=some_user, model=book)
history_entries = BookEditHistory.get_history_entry(history_entry_instance) for history_entry_instance in history_entry_instancesTroubleshooting custom m2m handlingWhen there is risk of sending by Django both signals model related (pre_save, post_delete etc.) and m2m related usewicked_historian.signals_exclusion.signal_exclusionand make those changes insignal_exclusion.model_signals_exclusion_contextcontext. When in context callingsignal_exclusion.are_model_signals_excludedwith the same arguments context was created returnsTrue.LicenseThe Django Wicked Historian package is licensed under theFreeBSD
License.