History functionality¶
Configuration¶
To enable history tracking, you must configure the models that will be monitored.
This is done in your settings.py file by adding them as app.model references:
# history
GT_HISTORY_ALLOWED_MODELS = [
"djgentelella.trash", # always add trash when you use history
"demoapp.customer",
# add more models here for history
]
History Filter Form¶
A filter form can be created to allow selecting categories when viewing history:
from django import forms
from djgentelella.forms.forms import GTForm
from djgentelella.widgets import core as genwidgets
from django.utils.translation import gettext_lazy as _
class HistoryFilterForm(GTForm, forms.Form):
category = forms.ChoiceField(
choices=[
("--------", _("All")),
("demoapp.customer", _("Customer")),
("djgentelella.trash", _("Trash")),
# add more categories here
],
widget=genwidgets.Select,
label=_("Category"),
)
History Template¶
The template defines how the history page will be displayed. In this example, we extend the base layout and render the history table:
{% extends 'gentelella/base.html' %}
{% load static i18n %}
{# page title #}
{% block title %} {% trans 'History' %} {% endblock %}
{# page content #}
{% block content %}
<h1 class="text-center"> {% trans 'History Example' %} </h1>
<hr>
<div class="card mt-2">
<div class="card-body">
<div class="card-title titles">
<h2 class="text-center"> {% trans 'History' %} </h2>
</div>
{# table history #}
<div class="row mt-3 ">
<div class="col-6">
{{ form }}
</div>
<div class="col-12">
<table class="table table-hover table-striped w-100 table-responsive" id="table-history">
</table>
</div>
</div>
</div>
</div>
{% endblock %}
JavaScript Initialization¶
Finally, we define the JavaScript logic to configure the datatable, filters, and actions:
const choices_actions = [
[1, gettext("Addiction")],
[2, gettext("Modification")],
[3, gettext("Deletion")],
[4, gettext("Hard deletion")],
[5, gettext("Restoration")]
]
const selects2_urls = {
users: "{% url 'users-list' %}",
}
const object_urls = {
list_url: "{% url 'api-history-list' %}",
}
const datatable_inits = {
columns: [
{
data: "action_time", name: "action_time", title: gettext("Date"), type: "date",
"dateformat": document.datetime_format, visible: true
},
{
data: "user", name: "user", title: gettext("User"), url: selects2_urls['users'],
type: "select2", visible: true
},
{data: "change_message", name: "change_message", title: gettext("Detail"), type: "string", visible: true},
{data: "object_repr", name: "object_repr", title: gettext("Information"), type: "string", visible: true},
{
data: "action_flag", name: "action_flag", title: gettext('Action'), type: "select",
choices: choices_actions, visible: true
},
{data: "actions", name: "actions", title: gettext("Actions"), type: "string", visible: false}
],
addfilter: true,
events: {
'filter': function (data) {
data['contenttype'] = document.getElementById("id_category").value;
return data
}
}
}
const modalids = {}
const actions = {
table_actions: [],
object_actions: [],
title: gettext('Actions'),
className: "no-export-col"
}
const icons = {
clear: '<i class="fa fa-eraser" aria-hidden="true"></i>',
}
const objconfig = {
datatable_element: "#table-history",
modal_ids: modalids,
actions: actions,
datatable_inits: datatable_inits,
add_filter: true,
relation_render: {'field_autocomplete': 'text'},
delete_display: '',
create: "btn-success",
icons: icons,
urls: object_urls
}
const ocrud = ObjectCRUD("crudobj", objconfig)
ocrud.init();
// added event to form
$('#id_category').on('select2:select', function (e) {
ocrud.datatable.ajax.reload();
})
Writing entries: add_log¶
djgentelella.history.utils.add_log writes one audit entry into Django’s
LogEntry and returns it:
from djgentelella.history.utils import add_log, ADDITION
entry = add_log(
request.user, instance, ADDITION,
change_message='Created from the import wizard',
related_objects=[project, (organization, {'role': 'owner'})],
extra={'batch': 42},
)
related_objects: a single instance, an iterable of instances, or(instance, data_dict)pairs. Each one becomes aHistoryRelationrow (generic FK + optional JSON annotation) — the way to say “this action also touched these other objects”. Bare pks are rejected: a number does not identify a model.extra: an arbitrary dict stored as a relation row with no target. Query it back throughLogEntry.gt_relations.If
change_messageis empty a generic sentence is generated; a caller-provided message is always kept (the field list is appended on create/update).Anonymous or missing users are attributed to the account named by the
GT_HISTORY_ANONYMOUS_USERNAMEsetting; without it,add_lograisesValueError(LogEntry.usercannot be NULL).
Automatic logging: BaseViewSetWithLogs¶
Subclass it instead of AuthAllPermBaseObjectManagement and every
create/update/destroy writes its entry. Optional knobs:
class CustomerViewSet(BaseViewSetWithLogs):
# None (default) logs every model this viewset touches;
# a list restricts logging to these labels.
models_log = ['demoapp.customer']
# Captured by default: browser, ip, method and path of the request.
log_request_metadata = True
def get_log_related_objects(self, instance):
return [instance.organization]
def get_log_extra(self, instance):
return {'source': 'import'}
If the instance inherits DeletedWithTrash, destroy soft-deletes it passing
user= so the trash records who threw it away — no override needed.
Filtering by relations and extra data¶
api-history-list understands these extra query parameters:
related_contenttype=app.model(+ repeatablerelated_id=N): entries whose relations point at those objects.extra={"org": 1, "area": 5}(JSON): entries whose relation payload contains all the given keys (1..n fields, matched on the same row).
Multi-tenant scoping¶
HistoryViewSet.scope_queryset(queryset) is the override point: it runs
last in get_queryset and recordsTotal is computed from the scoped
universe (never from the global table).
class OrgHistoryViewSet(HistoryViewSet):
def scope_queryset(self, queryset):
org_ct = ContentType.objects.get_for_model(Organization)
return queryset.filter(
gt_relations__content_type=org_ct,
gt_relations__object_id=self.kwargs['org_pk'],
).distinct()