Event Tracking¶
Here we overview how event tracking works and how to customize it. It's useful to read both the Quick Start and Basics sections for a primer on terminology and concepts.
pghistory.track¶
pghistory.track is the primary way to track model events. For example, let's say that we have the following model:
class TrackedModel(models.Model):
int_field = models.IntegerField()
char_field = models.CharField(max_length=16, db_index=True)
user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
Now let's track snapshots for every insert and update of TrackedModel with pghistory.track:
By default, pghistory.track registers pghistory.InsertEvent and pghistory.UpdateEvent trackers. See custom trackers below for how to override this behavior.
When trackers are added, an auto-generated event model is created and populated by triggers installed by django-pgtrigger. By default, the model will be named TrackedModelEvent. It contains every field in TrackedModel plus a few additional tracking fields.
For example, let's create a TrackedModel, update it, and print the resulting event values:
from myapp.models import TrackedModel
m = TrackedModel.objects.create(int_field=1, text_field="hello")
m.int_field = 2
m.save()
# "events" is the default related name of the event model.
print(m.events.values("pgh_obj", "pgh_label", "int_field"))
> [
> {'pgh_obj': 1, 'pgh_label': 'insert', 'int_field': 1},
> {'pgh_obj': 1, 'pgh_label': 'update', 'int_field': 2}
> ]
Tracking Specific Fields¶
One may wish to only track a subset of fields in a model. This can be achieved by using the fields and exclude arguments to pghistory.track. For example, here we create an event model that only tracks int_field:
@pghistory.track(fields=["int_field"])
class TrackedModel(models.Model):
int_field = models.IntegerField()
char_field = models.CharField(max_length=16, db_index=True)
user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
When doing this, keep the following in mind:
- The event model will only mirror the
int_fieldof the tracked model. It won't storechar_fieldoruser. - Updates to the event model will only create events if
int_fieldis changed.
One can also exclude fields like so:
@pghistory.track(exclude=["int_field"])
class TrackedModel(models.Model):
int_field = models.IntegerField()
char_field = models.CharField(max_length=16, db_index=True)
user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
In this case, we'll track both char_field and user. Inserts and updates to either of these two fields will create events.
Note
One can further override event model configuration, such as creating custom database indices, by directly creating a custom event model. See this section for more details.
Custom Trackers¶
We've only shown pghistory.track with the default configuration. Let's say that we wish to only create events on model deletion. We can configure this behavior using the pghistory.DeleteEvent tracker:
Remember
We've overridden the default trackers by doing this. Only deletions will be tracked. One can add back in the default behavior of tracking inserts and updates as follows:
The pghistory.InsertEvent, pghistory.UpdateEvent, and pghistory.DeleteEvent trackers all inherit pghistory.RowEvent and specify some defaults, such as:
- The
pgh_labelthat will be generated. pghistory.DeleteEvent, for example, will use "delete" as the label of the event. Customize this by providing the label as the first argument, i.e.pghistory.DeleteEvent("my_custom_label"). - The row and trigger conditions. pghistory.UpdateEvent, for example, has its condition configured to only fire if any tracked fields change. It also stores the
NEWrow of the update. If one desires to store the row as it was before the update, dopghistory.UpdateEvent(row=pghistory.Old).
Conditional Tracking¶
By default, pghistory.track stores events for all changes to the fields specified (or every field if no fields are specified). Supply conditional trackers to specify when events are created. We show examples of this below.
Tip
All examples here mostly pass through to django-pgtrigger's conditional interface. Check out those docs for more examples.
Basic Example¶
Here we create a conditional tracker that only fires whenever the email is updated:
import pghistory
@pghistory.track(
pghistory.UpdateEvent(
"email_changed",
row=pghistory.Old,
condition=pghistory.AnyChange("email")
),
model_name="UserEmailHistory"
)
class MyUser(models.Model):
username = models.CharField(max_length=128)
email = models.EmailField()
address = models.TextField()
There are two key things going on here:
- The pghistory.UpdateEvent tracker runs on updates of
MyUser, storing what the row looked like right before the update (i.e. the "old" row). - We use pghistory.AnyChange to specify that the event should fire on any change to
email.
Let's see what this looks like when we change the email field:
from myapp.models import MyUser, UserEmailHistory
u = MyUser.objects.create(
username="hello",
email="hello@hello.com",
address="123 Main St"
)
# Events are only tracked on updates, so nothing has been stored yet
assert not UserEmailHistory.objects.exists()
# Change the address. An event is not created
u.address = "456 Main St"
u.save()
assert not UserEmailHistory.objects.exists()
# Change the email. An event should be stored
u.email = "world@world.com"
u.save()
print(UserEmailHistory.objects.filter(pgh_obj=u).values_list("email", flat=True))
> ["hello@hello.com"]
Condition Utilities¶
django-pghistory provides the following utilities for creating change conditions, all of which are from the django-pgtrigger library:
- pghistory.AnyChange fires when any fields change.
- pghistory.AnyDontChange fires when any fields don't change.
- pghistory.AllChange fires when all fields change.
- pghistory.AllDontChange fires when all fields don't change.
Here are some brief examples:
pghistory.AnyChange("field_one", "field_two"): Fire whenfield_oneorfield_twochange.pghistory.AnyChange(exclude=["my_field"]): Fire when any tracked field except formy_fieldchanges.pghistory.AnyChange(exclude_auto=True): Excludeauto_now=Trueorauto_now_add=Truefields (e.g.DateFieldandDateTimeField).pghistory.AllChange("field_three", "field_four"). Fire only when bothfield_threeandfield_fourchange in the same update.
Remember, the tracked fields are the default if no fields are provided and the starting point before excluding fields. For example, here we are tracking changes to char_field and int_field:
@pghistory.track(
pghistory.UpdateEvent(
condition=pghistory.AnyChange()
),
fields=["char_field", "int_field"]
)
class TrackedModel(models.Model):
int_field = models.IntegerField()
char_field = models.CharField(max_length=16, db_index=True)
user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
Events can still be fired for untracked fields, but they must be explicitly provided, for example, pghistory.AnyChange("user").
Remember
The conditions above are only for pghistory.UpdateEvent trackers. They cannot be used on pghistory.InsertEvent or pghistory.DeleteEvent since rows aren't being changed.
Q and F Conditions¶
We can create even more specific conditions with the pghistory.Q and pghistory.F constructs, which are also from the django-pgtrigger library. For example, let's make an event when the cash in a bank account drops below one hundred dollars:
@pghistory.track(
pghistory.UpdateEvent(
"money_below_one_hundred",
condition=pghistory.Q(old__dollars__gte=100, new__dollars__lt=100)
),
)
class Cash(models.Model):
dollars = models.DecimalField()
We can make another event log for when the money goes down or up:
@pghistory.track(
pghistory.UpdateEvent(
"money_below_one_hundred",
condition=pghistory.Q(old__dollars__gte=100, new__dollars__lt=100)
),
pghistory.UpdateEvent(
"money_down",
condition=pghistory.Q(old__dollars__gt=pghistory.F("new__dollars"))
),
pghistory.UpdateEvent(
"money_up",
condition=pghistory.Q(old__dollars__lt=pghistory.F("new__dollars"))
)
)
class Cash(models.Model):
dollars = models.DecimalField()
See the django-pgtrigger docs to learn more about trigger conditions and how the Q and F objects can be used.
Multiple Trackers¶
Use multiple invocations of pghistory.track to track events with different schemas:
@pghistory.track(
pghistory.UpdateEvent(
"email_changed",
row=pghistory.Old,
condition=pghistory.AnyChange("email")
),
fields=["email"],
model_name="UserEmailHistory"
)
@pghistory.track(
pghistory.UpdateEvent(
"username_changed",
row=pghistory.Old,
condition=pghistory.AnyChange("username")
),
fields=["username"],
model_name="UserUsernameHistory"
)
class TrackedModel(models.Model):
...
Manual Tracking¶
Sometimes it is not possible to express an event based on a series of changes to a model. Some use cases, such as backfilling data, also require that events are manually created.
pghistory.create_event can be used to manually create events. Events can be created for existing trackers, or the bare pghistory.ManualEvent can be used for registering events that can only be manually created.
Here we register a bare pghistory.ManualEvent tracker and create an event with the label of "user.create":
@pghistory.track(
pghistory.ManualEvent("user_create"),
fields=['username']
)
class MyUser(models.Model):
username = models.CharField(max_length=64)
password = models.PasswordField()
# Create a user and manually create a "user.create" event
user = MyUser.objects.create(...)
pghistory.create_event(user, label="user_create")
Note
Manually-created events will still be linked with context if context tracking has started. More on context tracking in the Collecting Context section.
Third-Party Models¶
django-pghistory can track changes to third-party models like Django's User model by using a proxy model. Below we show how to track the default Django User model:
from django.contrib.auth.models import User
import pghistory
# Track the user model, excluding the password field
@pghistory.track(exclude=["password"])
class UserProxy(User):
class Meta:
proxy = True
Important
Although it's possible to track the models directly with pghistory.track(...)(model_name), doing so would create migrations in a third-party app. Using proxy models ensures that the migration files are created inside your project.
Many-To-Many Fields¶
Events in many-to-many fields, such as user groups or permissions, can be configured by tracking the "through" model of the many-to-many relationship. Here we show an example of how to track group "add" and "remove" events for Django's user model:
from django.contrib.auth.models import User
import pghistory
@pghistory.track(
pghistory.AfterInsert("group.add"),
pghistory.BeforeDelete("group.remove"),
obj_field=None,
)
class UserGroups(User.groups.through):
class Meta:
proxy = True
There are a few things to keep in mind:
- We made a proxy model since it's a third-party model. Models in your project can directly call
pghistory.track(arguments)(model). - Django does not allow foreign keys to auto-generated "through" models. We set
obj_field=Noneto ignore creating a reference in the event model. See the Configuring Event Models section for more information.
After migrating, events will be tracked as shown:
# Note: this is pseudo-code
> user = User.objects.create_user("username")
> group = Group.objects.create(name="group")
> user.groups.add(group)
> user.groups.remove(group)
> print(my_app_models.UserGroupsEvent.objects.values("pgh_label", "user", "group"))
[
{"user": user.id, "group": group.id, "pgh_label": "group.add"},
{"user": user.id, "group": group.id, "pgh_label": "group.remove"},
]