django-simple-history

repository·master·Indexed 25 days ago

https://github.com/django-commons/django-simple-history

A Django application that tracks and stores the state of Django models during create, update, and delete operations to provide a full audit trail. It includes integration with the Django admin site for viewing and reverting model versions, utilities for bulk creation and updates with history, and customizable historical model configurations including custom field types, table names, and indexing options.

Tokens
14.5K
Snippets
38
Records
71
Agent score
82%

What's inside django-simple-history

  1. Record which user changed a model

    master

    To track which user is responsible for changes to a model, django-simple-history provides four primary methods:

    1. HistoryRequestMiddleware: Automatically sets the history_user on the history table to the User instance making the current request.
    2. SimpleHistoryAdmin: When using the Django admin, this class automatically sets the _history_user on the object by overriding save_model.
    3. _history_user attribute: Manually assign a user to the _history_user attribute on your model instance.
    4. Manual User Tracking: Use an explicit history_user_id for scenarios where the user model lives in a different database or is accessed via an external API.
  2. Use `HistoricForeignKey` and `HistoricOneToOneField` for time-aware relations

    master

    Standard Django foreign keys point to the current state of a related object. If you are traversing history using as_of, you want the related object to also reflect the state it had at that specific point in time.

    Use HistoricForeignKey or HistoricOneToOneField to ensure that chasing relationships from an as_of acquired instance honors the historical point in time for both forward and reverse relationships.

  3. How multi-table inheritance works with history

    master

    You can track history on models using multi-table inheritance.

    Key Behaviors:

    • Independence: HistoricalRecords is not inherited. You must explicitly add it to the parent, the child, or both if you want to track both.
    • Data Scope: The child's history table contains all fields from both the child and the parent models.
    • Update Isolation: Updating a child instance only updates the child's history table; it does not update the parent's history table.
    class ParentModel(models.Model):
        parent_field = models.CharField(max_length=255)
        history = HistoricalRecords()
    
    class ChildModel(ParentModel):
        child_field = models.CharField(max_length=255)
        # history is NOT inherited automatically
  4. How ModelDelta handles ForeignKeys and Many-to-Many fields

    master

    The output of diff_against() varies significantly based on the foreign_keys_are_objs flag:

    ForeignKey Fields

    • Default (False): Returns the raw primary key (e.g., 'poll' changed from '15' to '31').
    • With Objects (True): Returns the model instance (e.g., 'poll' changed from '<Poll: what's up?>' to '<Poll: still around?>').

    Many-to-Many Fields

    • Default (False): Returns a list of dictionaries containing through-model field names and primary keys (e.g., [{'poll': 15, 'category': 63}]).
    • With Objects (True): Returns a list of dictionaries where the values are the actual model instances (e.g., [{'poll': <Poll: what's up?>, 'category': <Category: informal questions>}]).

    Deleted Objects

    If an object is deleted, foreign_keys_are_objs=True will represent it as a DeletedObject (e.g., DeletedObject(model=<class 'models.Category'>, pk=63)).

  5. Understand the historical data schema

    master

    When you enable history tracking, django-simple-history creates a new table for each tracked model, prefixed with historical. These tables store a snapshot of the model's fields plus the following metadata fields:

    • history_user: The user who performed the change.
    • history_date: The datetime of the change.
    • history_change_reason: The reason for the change (defaults to null).
    • history_id: The primary key for the historical record (note that the base model's PK is not unique in this table).
    • history_type: The type of operation: + for create, ~ for update, and - for delete.
  6. Track User in a Separate Database

    master
    If your history tables reside in a different database than your User model, you cannot use standard Django foreign key relations because Django does not support cross-database relations. In this scenario, you must manually track the history_user using an explicit ID instead of a model instance. Refer to the 'Manually Track User Model' documentation for implementation details.
  7. Revert a model instance to a previous state

    master

    To programmatically revert a model instance to a state captured in a historical record, retrieve the historical object and call .save() on its .instance attribute. This updates the live model with the historical data and creates a new historical record representing this reversion.

    # Revert to the earliest version
    >>> earliest_poll = poll.history.earliest()
    >>> earliest_poll.instance.save()
  8. Query history on a model instance or class

    master

    You can access historical records through the HistoricalRecords object.

    • On an instance: Use instance.history to access a manager for that specific object's history (e.g., poll.history.all()).
    • On a class: Use Model.history to access a manager for all historical records across all instances of that model (e.g., Choice.history.all()).

    Because the history is a model, you can use standard Django QuerySet methods like .filter() on the history manager.

  9. Track many-to-many relationships

    master

    By default, many-to-many (M2M) fields are ignored by history tracking. To track changes in M2M relationships, you must explicitly list them in the m2m_fields argument of HistoricalRecords.

    This creates a historical intermediate model that tracks relational changes. You can pass either the field name (string) or the field instance itself.

    class Category(models.Model):
        name = models.CharField(max_length=200)
    
    class Poll(models.Model):
        question = models.CharField(max_length=200)
        categories = models.ManyToManyField(Category)
        history = HistoricalRecords(m2m_fields=[categories])
  10. Use signals to provide custom behavior for historical records

    master

    django-simple-history provides signals that allow you to execute custom logic whenever a historical record is created or updated. You can connect your callbacks using the standard Django @receiver decorator.

    Standard Model Signals

    When a historical record for a single model instance is being processed, the following arguments are passed to the signal receiver:

    • instance: The source model instance being saved.
    • history_instance: The corresponding history record.
    • history_date: Datetime of the history record's creation.
    • history_change_reason: Freetext description of the reason for the change.
    • history_user: The user that instigated the change.
    • using: The database alias being used.

    Many-to-Many (M2M) Signals

    For signals related to Many-to-Many fields, the following additional arguments are available:

    • instance: The source model instance being saved.
    • history_instance: The corresponding history record.
    • rows (available in pre_create_historical_m2m_records): The elements to be bulk inserted into the m2m table.
    • created_rows (available in post_create_historical_m2m_records): The created elements into the m2m table.
    • field: The recorded field object.
    from django.dispatch import receiver
    from simple_history.signals import (
        pre_create_historical_record,
        post_create_historical_record,
        pre_create_historical_m2m_records,
        post_create_historical_m2m_records,
    )
    
    @receiver(pre_create_historical_record)
    def pre_create_historical_record_callback(sender, **kwargs):
        print("Sent before saving historical record")
    
    @receiver(post_create_historical_record)
    def post_create_historical_record_callback(sender, **kwargs):
        print("Sent after saving historical record")
    
    @receiver(pre_create_historical_m2m_records)
    def pre_create_historical_m2m_records_callback(sender, **kwargs):
        print("Sent before saving many to many field on historical record")
    
    @receiver(post_create_historical_m2m_records)
    def post_create_historical_m2m_records_callback(sender, **kwargs):
        print("Sent after saving many to many field on historical record")