Tying in to Django Admin's Model History
Asked Answered
A

6

115

The Setup:

  • I'm working on a Django application which allows users to create an object in the database and then go back and edit it as much as they desire.
  • Django's admin site keeps a history of the changes made to objects through the admin site.

The Question:

  • How do I hook my application in to the admin site's change history so that I can see the history of changes users make to their "content"?
Amaris answered 12/6, 2009 at 16:32 Comment(0)
R
159

The admin history is just an app like any other Django app, with the exception being special placement on the admin site.

The model is in django.contrib.admin.models.LogEntry.

When a user makes a change, add to the log like this (stolen shamelessly from contrib/admin/options.py:

from django.utils.encoding import force_unicode
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin.models import LogEntry, ADDITION
LogEntry.objects.log_action(
    user_id         = request.user.pk, 
    content_type_id = ContentType.objects.get_for_model(object).pk,
    object_id       = object.pk,
    object_repr     = force_unicode(object), 
    action_flag     = ADDITION
)

where object is the object that was changed of course.

Now I see Daniel's answer and agree with him, it is pretty limited.

In my opinion a stronger approach is to use the code from Marty Alchin in his book Pro Django (see Keeping Historical Records starting at page 263). There is an application django-simple-history which implements and extends this approach (docs here).

Reticle answered 12/6, 2009 at 18:20 Comment(8)
Don't forget: from django.contrib.contenttypes.models import ContentType. Also, force_unicode is also their own function.Lines
from django.utils.encoding import force_unicode for 'force_unicode'Hynda
Since this question was answered, Marty Alchin's approach has been open sourced and extended in an application called django-simple-history.Inscription
The new home of django-simple-history seems to be: github.com/treyhunner/django-simple-history More info on RTD django-simple-history.readthedocs.org/en/latestCalcaneus
A good approach may also to check the comparison grid at djangopackages.com where django-simple-history and other solutions (like CleanerVersion or django-reversion) are compared.Sparling
The section on Keeping Historical Records is only in the first edition of the Pro Django book.Goldsmith
it's possible to use a list of object_it to filter ?Stav
force_unicode has been deprecated in favor of force_text from django.utils.encodingNitrogen
O
27

The admin's change history log is defined in django.contrib.admin.models, and there's a history_view method in the standard ModelAdmin class.

They're not particularly clever though, and fairly tightly coupled to the admin, so you may be best just using these for ideas and creating your own version for your app.

Oblation answered 12/6, 2009 at 18:13 Comment(1)
Is this still true?Dowdy
O
12

I know this question is old, but as of today (Django 1.9), Django's history items are more robust than they were at the date of this question. In a current project, I needed to get the recent history items and put them into a dropdown from the navbar. This is how I did it and was very straight forward:

*views.py*    

from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION

def main(request, template):

    logs = LogEntry.objects.exclude(change_message="No fields changed.").order_by('-action_time')[:20]
    logCount = LogEntry.objects.exclude(change_message="No fields changed.").order_by('-action_time')[:20].count()

    return render(request, template, {"logs":logs, "logCount":logCount})

As seen in the above code snippet, I'm creating a basic queryset from the LogEntry model (django.contrib.admin.models.py is where it's located in django 1.9) and excluding the items where no changes are involved, ordering it by the action time and only showing the past 20 logs. I'm also getting another item with just the count. If you look at the LogEntry model, you can see the field names that Django has used in order to pull back the pieces of data that you need. For my specific case, here is what I used in my template:

Link to Image Of Final Product

*template.html*

<ul class="dropdown-menu">
    <li class="external">
        <h3><span class="bold">{{ logCount }}</span> Notification(s) </h3>
        <a href="{% url 'index' %}"> View All </a>
    </li>
        {% if logs %}
            <ul class="dropdown-menu-list scroller actionlist" data-handle-color="#637283" style="height: 250px;">
                {% for log in logs %}
                    <li>
                        <a href="javascript:;">
                            <span class="time">{{ log.action_time|date:"m/d/Y - g:ia" }} </span>
                            <span class="details">
                                {% if log.action_flag == 1 %}
                                    <span class="label label-sm label-icon label-success">
                                        <i class="fa fa-plus"></i>
                                    </span>
                                {% elif log.action_flag == 2 %}
                                    <span class="label label-sm label-icon label-info">
                                        <i class="fa fa-edit"></i>
                                    </span>
                                {% elif log.action_flag == 3 %}
                                    <span class="label label-sm label-icon label-danger">
                                        <i class="fa fa-minus"></i>
                                    </span>
                                {% endif %}
                                {{ log.content_type|capfirst }}: {{ log }}
                            </span>
                        </a>
                    </li>
                 {% endfor %}
            </ul>
        {% else %}
            <p>{% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}</p>
        {% endif %}
    </li>
</ul>
Oldfashioned answered 6/3, 2016 at 21:58 Comment(0)
C
7

To add to what's already been said, here are some other resources for you:

(1) I've been working with an app called django-reversion which 'hooks into' the admin history and actually adds to it. If you wanted some sample code that would be a good place to look.

(2) If you decided to roll your own history functionality django provides signals that you could subscribe to to have your app handle, for instance, post_save for each history object. Your code would run each time a history log entry was saved. Doc: Django signals

Crean answered 31/7, 2009 at 0:16 Comment(4)
I would strongly recommend against django-reversion. In concept, it's a great idea, but the implementation is terrible. I used this in a production site and it was a nightmare. It worked great at first, but I eventually found out that the app's not at all scalable, so for any models with semi-frequent changes, your admin will become unusable in a few months because the queries it uses are horribly inefficient.Symploce
@Symploce and others: is this still true? I am trying to see if I can use django-reversion for a site with lots of content. django-reversion seems to be top-rated on djangopackages.org and SO posts, but being-able-to-scale is an important priority for my app, hence askingConfront
@Anupam, I haven't used it since I had to disable it from my production site. I reported the issues as a bug, but the dev blew me off and said it wasn't a problem, so I haven't re-evaluated the project.Symploce
I see - do you mind sharing the issue link please? Will be super helpful for me since I am seriously considering whether to use it or not for my Django appConfront
R
3

Example Code

Hello,

I recently hacked in some logging to an "update" view for our server inventory database. I figured I would share my "example" code. The function which follows takes one of our "Server" objects, a list of things which have been changed, and an action_flag of either ADDITION or CHANGE. It simplifies things a wee bit where ADDITION means "added a new server." A more flexible approach would allow for adding an attribute to a server. Of course, it was sufficiently challenging to audit our existing functions to determine if a changes had actually taken place, so I am happy enough to log new attributes as a "change".

from django.contrib.admin.models import LogEntry, User, ADDITION, CHANGE
from django.contrib.contenttypes.models import ContentType

def update_server_admin_log(server, updated_list, action_flag):
    """Log changes to Admin log."""
    if updated_list or action_flag == ADDITION:
        if action_flag == ADDITION:
            change_message = "Added server %s with hostname %s." % (server.serial, server.name)
        # http://dannyman.toldme.com/2010/06/30/python-list-comma-comma-and/
        elif len(updated_list) > 1:
            change_message = "Changed " + ", ".join(map(str, updated_list[:-1])) + " and " + updated_list[-1] + "."
        else:
            change_message = "Changed " + updated_list[0] + "."
        # https://mcmap.net/q/188231/-tying-in-to-django-admin-39-s-model-history
        try:
            LogEntry.objects.log_action(
                # The "update" user added just for this purpose -- you probably want request.user.id
                user_id = User.objects.get(username='update').id,
                content_type_id = ContentType.objects.get_for_model(server).id,
                object_id = server.id,
                # HW serial number of our local "Server" object -- definitely change when adapting ;)
                object_repr = server.serial,
                change_message = change_message,
                action_flag = action_flag,
                )
        except:
            print "Failed to log action."
Recreate answered 12/6, 2009 at 16:32 Comment(0)
D
1

Example code:

from django.contrib.contenttypes.models import ContentType  
from django.contrib.admin.models import LogEntry, ADDITION  

LogEntry.objects.log_action(
    user_id=request.user.pk,
    content_type_id=ContentType.objects.get_for_model(object).pk,
    object_id=object.pk,
    object_repr=str(object),
    action_flag=ADDITION,
)

Object is the object you want to register in the admin site log.
You can try with str() class in the parameter object_repr.

Duston answered 29/11, 2020 at 5:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.