django-changesets

https://drone.io/bitbucket.org/aquavitae/django-changesets/status.png

Warning

This app is very much a work in progress, and far from complete or stable. This documentation is currently just a roadmap and sections of it have not yet been implemented. Once the documentation matches the code this notice will be removed.

If anyone would like to contribute to the code, fork it and create a pull request.

django-changesets is a history tracking app for Django.

Installation

The easiest is to install with pip:

pip install django-changesets

Configuration

django-changesets requires the auth module, so the first configuration step is to add the necessary apps to INSTALLED_APPS:

INSTALLED_APPS = (
    ...
    'django.contrib.auth',
    'changesets',
    ...
)

Optionally, install the middleware after the auth middleware (see Middleware for details):

MIDDLEWARE_CLASSES = (
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'changesets.middleware.ChangeSetMiddleware',
    ...
)

If the models already contain data, then bring create an initial changeset by running:

with record_changeset(comment='Initial data'):
    scan('myapp')

Usage

This app aims be provide a flexible and stable method of recording changes to data by encapsulating a group of changes into a ChangeSet, stamped with the time and user who made the changes.

One of the more powerful features provided is the ability of record a changeset after the fact (this also makes it very easy to add the app to existing data). Since every change is simply a database record, the history is also mutable, although changes to the history need to be done with care since they could result in invalid data (e.g. broken relationships).

This is designed to work with all relationship fields, including ForeignKey, ManyToManyField and GenericForeignKey.

Model Overview

The primary model in this app is ChangeSet. This represents a collection of individual changes made at a specific point in time by a single user. Individual changes are recorded by the object, field, and value changed.

To ensure stability of the changesets, changed model instances are not referenced directly, instead they are wrapped in an ObjectWrapper model. The reason for this is that records in this model are never deleted, meaning that changes relating to deleted objects can be kept and still be sensibly queried.

Fields are represented by FieldType, which is a bit like a ContentType, but for fields instead of models.

Changed values are converted to text and stored in a TextField.

Tracking Changes

The most basic method of recording a changeset is through record_changeset. For example:

with record_changeset(user=my_user, comment='Some changes'):
    # Change a value
    obj.field = 2
    obj.save()
    # Add something
    MyModel.objects.create(value='new object')
    # Delete something
    old_obj.delete()
    # For a bulk operation we need to find the changes are they are made
    queryset.update(my_value='new value')
    scan(queryset)

Middleware

Often, changesets will be wanted for all changes made by a user through a view, and this can be implemented simply by adding ChangeSetMiddleware below AuthenticationMiddleware in MIDDLEWARE_CLASSES:

MIDDLEWARE_CLASSES = (
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'changesets.middleware.ChangeSetMiddleware',
    ...
)

This has the effect of wrapping every view in a ChangeSet and automatically assigning the current user. Note that bulk operations must still be dealt with explicitly.

Introspection

If changes were made and not recorded (often because of a bulk operation, or after setting up changesets on an existing project), they can be pulled into a changeset afterwards using scan. For example:

with record_changeset(comment='Bulk operations just happened'):
    # We know which objects were only added and changed
    scan(changed_queryset)
    # We know that there were only deletions here
    scan(OtherModel, delete_only=True)
    # Lot of stuff happened in this model
    scan(MessyModel)
    # This entire app has changes
    scan('myapp')

Querying History

ChangeSets are just models, so they can be queried just the same as any other model.