Overview

django-historymodel

This pluggable Django app providers an easy way of creating models that will keep snapshots of other models.

WARNING This is project is still in early stages of development. Some features have not been tested extensively, and there is no test suite. Use it at your own risk.

Installation

This project is not yet packaged for distribution on PyPI. Expect the package to become available as soon as a complete test suite is written.

Please note that this app should not be included in the INSTALLED_APPS.

Basic usage

This is just a short note to people who feel adventurous enough to try it out. We've actually started using it for one of our projects, but it is still quite far from being complete.

The simplest use case is to create two models. One is the normal model that you will use as usual. Another model is the history model, which keeps track of your original model's changes.

from django.db import models
from historymodel.models import HistoryModel


class MyModel(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updatred_at = models.DateTimeField(auto_now=True)


class MyModelHistory(HistoryModel):
    pass

The above snippet is the simplest scenario. We name our history model after our main model by appending 'History' at the end of the name. This makes django-historymodel derive the main model's class name.

The process of creating the MyModelHistory class involves registration of a post-save signal that will record the changes every time MyModel is saved using the save() method. Note that this means using update() will not cause the history to be recorded.

Each record of the MyModelHistory will have a timestamp field which corresponds to its creation time. Instances will also have an _original_model property which points to the _class_ of the source model, as well as the _original_model_name, which points to the class _name_ of the source model.

Note that auto_now* fields will be excluded from the history, as well as any related models using either ForeignKey or ManyToMany fields. The auto-timestamp fields do not make much sense anyway, since history model has its own timestamp. As for related fields, it is implied that you cannot track changes in relationships directly (you can still track some aspects of the related objects by using the cache_fields meta option as discussed in Field caching section).

History admin

TODO

Customization

If you prefer to call your history model something other than ModelNameHistory, you can add the Meta class with original_model property.

class TimeMachine(HistoryModel):
    class Meta:
        original_model = MyModel

You can also use a string to refer to a model in the original_model property (note: not very well tested).

There are three more meta options that you can use.

The history_track_fields is an iterable that contains the names of the fields on the original model that you want to track. For example:

class MyModelHistory(HistoryModel):
    class Meta:
        history_track_fields = ('title',)

You can also exclude some fields using the history_exclude_fields:

class MyModelHistory(HistoryModel):
    class Meta:
        history_exclude_fields = ('body',)

Third meta option is the cache_fields option which is discussed in the Field caching section.

Field caching

Because we cannot track relationships by simply copying the foreign keys (that would make the app quite complex), we use cache fields instead. The cache fields map the fields from the original model one field in the history model either using simple declarations or a function.

The process of adding cache fields boils down to adding custom model fields to the history model which will cache the data, and adding declarations sa the meta option.

Let's take a look at an example:

class Author(models.Model):
    name = models.CharField(max_length=30)
    birth_year = models.PositiveSmallIntegerField()


class Book(models.Model):
    title = models.CharField(max_length=20)
    description = models.TextField()
    author = models.ForeignKey(Author)


class BookHistory(HistoryModel):
    author = models.CharField(max_length=30)

    class Meta:
        cache_fields = {
            'author': 'name'
        }

The above is probably the simplest setup. It caches the related Author instance's name field as the history model's author field. This is actually a little bit more complex than it seems. The author key in the cache_fields dictionary is actually the name of the Book model's field, and not the name of the BookHistory model's field. If the value of the key is a simple string, it is assumed that the history model's cache field will have the same name as the original model's field (author in our case).

What if the local field is not named the same as original field? Here's an example:

class BookHistory(HistoryModel):
    author_name = models.CharField(max_length=30)

    class Meta:
        cache_fields = {
            'author': {
                'original_field': 'name',
                'local_field': 'author_name',
            }
        }

By using a dictionary as the value, we can now specify separately the field we want to cache, and the name of the history model field to use as cache field.

If we need to use more than one field from the original model, we can pass a callable. In this case, the history model's field _must_ be named the same as the original object's field.

class BookHistory(HistoryModel):
    author = models.CharField(max_length=35)

    class Meta:
        cache_fields = {
            'author': lambda a: '%s %s' % (a.name, a.birth_year)
        }

Again, note that cache fields deal with _related_ objects, and not the instances of the original model for which history is being recorded.

Design considerations

TODO

Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.