Skip to main content

The Admin Log and History System

The Admin Log and History System in this codebase provides a robust auditing mechanism that tracks every addition, change, and deletion performed through the Django Admin interface. This system ensures accountability by recording who performed an action, when it happened, and exactly what was modified.

The LogEntry Model

The core of the auditing system is the LogEntry model, defined in django/contrib/admin/models.py. It functions as a specialized audit log that captures the state of an object at the time of an action.

Key Fields and Action Flags

Each LogEntry records several critical pieces of information:

  • user: A foreign key to the user who performed the action.
  • action_flag: An integer representing the type of action: ADDITION (1), CHANGE (2), or DELETION (3).
  • content_type and object_id: These fields form a generic relation to the object being modified, allowing the log to point to any model in the system.
  • object_repr: A string representation of the object (up to 200 characters) at the time of the action. This is crucial because it preserves the object's identity even if the original record is later deleted.
  • change_message: A text field that stores either a simple string or a structured JSON list describing the specific changes.

Intelligent Message Formatting

The LogEntry model includes a get_change_message() method that handles the complexity of turning stored data into human-readable text. If the change_message is a JSON structure, this method parses it and applies translations dynamically.

# django/contrib/admin/models.py

def get_change_message(self):
if self.change_message and self.change_message[0] == "[":
try:
change_message = json.loads(self.change_message)
except json.JSONDecodeError:
return self.change_message
messages = []
for sub_message in change_message:
if "added" in sub_message:
# ... logic to format addition messages ...
elif "changed" in sub_message:
# ... logic to format change messages including field names ...
elif "deleted" in sub_message:
# ... logic to format deletion messages ...

change_message = " ".join(msg[0].upper() + msg[1:] for msg in messages)
return change_message or gettext("No fields changed.")
else:
return self.change_message

This design choice allows the system to store change details in a language-agnostic format while providing localized output to the administrator viewing the history.

Recording Actions with LogEntryManager

The LogEntryManager provides a high-level log_actions method used by ModelAdmin to record events. This method is designed for efficiency, particularly when dealing with bulk operations.

Bulk vs. Single Object Logging

When multiple objects are modified (such as during a bulk deletion in the admin changelist), the manager uses bulk_create to minimize database hits.

# django/contrib/admin/models.py

def log_actions(self, user_id, queryset, action_flag, change_message="", *, single_object=False):
if isinstance(change_message, list):
change_message = json.dumps(change_message)

log_entry_list = [
self.model(
user_id=user_id,
content_type_id=ContentType.objects.get_for_model(obj, for_concrete_model=False).id,
object_id=obj.pk,
object_repr=str(obj)[:200],
action_flag=action_flag,
change_message=change_message,
)
for obj in queryset
]

if len(log_entry_list) == 1:
instance = log_entry_list[0]
instance.save()
return instance if single_object else [instance]

return self.model.objects.bulk_create(log_entry_list)

Tradeoff Note: Because bulk_create is used for multiple entries, pre_save and post_save signals are not dispatched for those LogEntry instances unless single_object=True is passed for a single-item queryset.

Constructing Change Messages

The detailed JSON structure stored in change_message is generated by the construct_change_message utility in django/contrib/admin/utils.py. This function inspects the ModelForm and any associated formsets (inlines) to determine exactly what changed.

It captures:

  1. Field-level changes: A list of labels for fields that were modified on the main object.
  2. Inline changes: Additions, modifications, or deletions of related objects within formsets.

The utility deactivates translations during construction (using translation_override(None)) to ensure that field names and model names are stored in their canonical form, allowing them to be translated correctly into the language of whichever administrator views the log later.

Accessing History in Templates

The admin interface exposes these logs through the "History" view of individual objects and the "Recent actions" sidebar on the admin index page.

The {% get_admin_log %} Template Tag

The AdminLogNode class implements the logic for the {% get_admin_log %} template tag. This tag allows developers to retrieve a subset of the log entries for display.

# django/contrib/admin/templatetags/log.py

class AdminLogNode(template.Node):
def render(self, context):
entries = context["log_entries"]
if self.user is not None:
user_id = self.user
if not user_id.isdigit():
user_id = context[self.user].pk
entries = entries.filter(user__pk=user_id)
context[self.varname] = entries[: int(self.limit)]
return ""

The log_entries variable used by this node is provided by the AdminSite via its each_context method, which calls get_log_entries(request). By default, this returns all LogEntry records with content_type and user pre-fetched to avoid N+1 query issues.

Example Template Usage

In admin/index.html, the tag is used to populate the "My actions" sidebar:

{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if admin_log %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% elif entry.is_deletion %}deletelink{% else %}changelink{% endif %}">
{% if entry.is_deletion or not entry.get_admin_url %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<br>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{{ entry.content_type.name }}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% translate 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}

This implementation demonstrates how the system balances detailed auditing (via JSON messages) with performance (via bulk operations and pre-fetching) and usability (via dynamic translation and utility methods like get_admin_url).