ChatGPT解决这个技术问题 Extra ChatGPT

Tying in to Django Admin's Model History

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"?


C
Community

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).


Don't forget: from django.contrib.contenttypes.models import ContentType. Also, force_unicode is also their own function.
from django.utils.encoding import force_unicode for 'force_unicode'
Since this question was answered, Marty Alchin's approach has been open sourced and extended in an application called django-simple-history.
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/latest
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.
D
Daniel Roseman

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.


Is this still true?
d
dave4jr

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>

J
James Emerton

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


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.
@Cerin 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 asking
@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.
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 app
d
dannyman

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] + "."
        # http://stackoverflow.com/questions/987669/tying-in-to-django-admins-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."

T
Tomás Agú

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.