pythondjangodjango-admincustomizationdjango-auditlog

django-auditlog change list customization in django admin


I've started using django-auditlog in my project. I was able to successfully log entries for changes to my model, however I want to make a tweak on how audit log displays change list in django admin. The problem is I've a ForeignKey in my Task model, it does have a __str__ method defined. When I make change to stage field on my Task model, the entry is logged, but it shows the stage id instead of stage.name as per defined in __str__ (i believe this is how it's supposed to work.

Here is my models.py

class Stage(models.Model):
    name = models.CharField(max_length=64)

    def __str__(self):
        return self.name

class Task(models.Model):
    name = models.CharField(max_length=64)
    stage = models.ForeignKey(Stage, on_delete=models.SET_NULL, null=True)
    
    ## other unrelated fields...

    def __str__(self):
        return self.name

Here is how the Log Entry is displayed currently: enter image description here

Here is the expected result: enter image description here

I've read the documentation of django-auditlog but couldn't find any solution. Any suggestion?


Solution

  • I was able to solve the issue by overriding LogEntryAdminMixin class.

    Here is what I did to solve the issue:

    1. Created a mixins.py file in my app folder with the following content:
    from auditlog.mixins import LogEntryAdminMixin
    from django.contrib import admin
    from django.utils.translation import gettext_lazy as _
    from django.utils.safestring import mark_safe
    import backend.api.models
    
    class CustomLogEntryAdminMixin(LogEntryAdminMixin):
        @admin.display(description=_("Changes"))
        def msg(self, obj):
            changes = obj.changes_dict
    
            atom_changes = {}
            m2m_changes = {}
    
            for field, change in changes.items():
                if isinstance(change, dict):
                    assert (
                        change["type"] == "m2m"
                    ), "Only m2m operations are expected to produce dict changes now"
                    m2m_changes[field] = change
                else:
                    atom_changes[field] = change
    
            msg = []
    
            if atom_changes:
                ## this is the part we are overriding
                ## This is the only change from the original code
                ## Change stage ids to stage names if obj is a Buy object
                if obj.content_type.model == "buy" and "stage" in atom_changes:
                    atom_changes["stage"] = [
                        backend.api.models.Stage.objects.get(id=stage_id).name
                        for stage_id in atom_changes["stage"]
                    ]
    
                msg.append("<table>")
                msg.append(self._format_header("#", "Field", "From", "To"))
                for i, (field, change) in enumerate(sorted(atom_changes.items()), 1):
                    value = [i, self.field_verbose_name(obj, field)] + (
                        ["***", "***"] if field == "password" else change
                    )
                    msg.append(self._format_line(*value))
                msg.append("</table>")
    
            if m2m_changes:
                msg.append("<table>")
                msg.append(self._format_header("#", "Relationship", "Action", "Objects"))
                for i, (field, change) in enumerate(sorted(m2m_changes.items()), 1):
                    change_html = format_html_join(
                        mark_safe("<br>"),
                        "{}",
                        [(value,) for value in change["objects"]],
                    )
    
                    msg.append(
                        format_html(
                            "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
                            i,
                            self.field_verbose_name(obj, field),
                            change["operation"],
                            change_html,
                        )
                    )
    
                msg.append("</table>")
    
            return mark_safe("".join(msg))
    
    1. Created a class an Admin class in my apps admin.py to use this mixin:
    from auditlog.admin import LogEntryAdmin
    from auditlog.models import LogEntry
    
    class CustomLogEnterAdmin(LogEntryAdmin, backend.api.mixins.CustomLogEntryAdminMixin):
        """
        This class is a custom LogEntryAdmin that extends the default LogEntryAdmin
        and uses custom mixins to customize the display of the changes field in the DAP for Buy objects stage field.
        """
        pass
    
    1. Unregister the default LogEntry class and register the CustomLogEntryAdmin class I just created.
    # unregister the default LogEntryAdmin and register the custom one
    admin.site.unregister(LogEntry)
    
    ## register custom log entry admin
    admin.site.register(LogEntry, CustomLogEnterAdmin)
    

    Output: enter image description here

    Now it shows ForeignKey model value, instead of keys.

    I hope it helps!