39

I'm using the standard django admin module to display a list of rows. One of the columns is a numerical field. I'd like to display an extra 'totals' row that has most of the columns as blank, except for the numerical column, which should be the total for all of the objects.

Is there a simple way to do this within the admin module, or am I better off making a custom view for it?

I'm using Django 1.2.

Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • Just to make sure: Do you mean the total sum for all the objects _on the current page_ or _all the objects in the database_? – Bernhard Vallant Jan 11 '12 at 11:03
  • @lazerscience: i mean of objects currently showing (so if you apply a filter it'll only show of the filtered items) – Claudiu Jan 11 '12 at 15:06

9 Answers9

42

Yes, you can do it in many ways, but most django-ist way to do is:

First override the default django listing view... And give a new template file directory

ModelAdmin.changelist_view(self, request, extra_context=None)

Like:

class MyModelAdmin(admin.ModelAdmin):

    # A template for a very customized change view:
    change_list_template = 'admin/myapp/extras/sometemplate_change_form.html'

    def get_total(self):
        #functions to calculate whatever you want...
        total = YourModel.objects.all().aggregate(tot=Sum('total'))['tot']
        return total

    def changelist_view(self, request, extra_context=None):
        my_context = {
            'total': self.get_total(),
        }
        return super(MyModelAdmin, self).changelist_view(request,
            extra_context=my_context)

So, you add your list view context a 'total' that keeps your total value and pass it to the template.

if change_list_template will set, django uses that template, otherwise, it uses standard django template.

If def changelist_view(self, request, extra_context=None) is called, django uses that function to create the content, otherwise it uses default django views.

Then create a admin/myapp/extras/sometemplate_change_form.html file and place your {{total}} to any place you want.

A guide to how to override admin templates And here is how to override admin views

UPDATE: I add a simple aggregate to calculate total. you can edit it to set it as your needs.

UPDATE 2: ModelAdmin template override option fixed from ModelAdmin.change_form_template to ModelAdmin.change_list_template. (thank you c4urself). Yes, but changing the default django admin template is a really bad choice, since it is used by many other ModelAdmin's and it might cause problems if related templates are updated.



NB:
The Total doesn't change when using filters, see comment below.

Mp0int
  • 18,172
  • 15
  • 83
  • 114
  • 2
    I think this is a great way to go, I believe `change_form_template` should be `change_list_template`. Though you don't need to give it a template name at all if you override the admin templates. – c4urself Jan 09 '12 at 09:49
  • 5
    Sweet, this is really a copy-paste recipe I was looking for. I'd just say, that instead of configuring `change_list_template` on the Admin class, I just create a file like: `templates/admin/my_app/my_model/change_list.html`, then in that file just use `{% extends 'admin/change_list.html' %}`, and only override a single block, like: `{% block result_list %} {{ block.super }}

    The total is: {{ total }}!!!

    {% endblock %}`. Convention over configuration ;)
    – Tomasz Gandor Jun 17 '14 at 12:33
  • even using `super(MyModelAdmin, self).get_queryset(request)`, the queryset doesn't get updated when using filters, the `Total` is always for the unfiltered list, this is not the case in c4urself's answer which I think is better and cleaner. – elsadek Dec 21 '14 at 21:30
39

I think the Django way to do this is to override the ChangeList class which django's admin app uses. You do this in django 1.2 by calling the get_changelist method in your admin class. In my example: TomatoAdmin calls this method returning a custom ChangeList calss. This ChangeList class: MyChangeList simply adds an extra attribute to the context of change_list.html.

Make sure that change_list.html is in the following directory:

app_label/change_list.html

in the example this is: templates/admin/tomato/change_list.html

models.py (A simple model with an integer field)

class CherryTomato(models.Model):
    name = models.CharField(max_length=100)
    num_in_box = models.IntegerField()

admin.py (your Admin class and a custom ChangeList class)

from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from django.db.models import Count, Sum

from tomatoes.tomato.models import CherryTomato

class MyChangeList(ChangeList):

    def get_results(self, *args, **kwargs):
        super(MyChangeList, self).get_results(*args, **kwargs)
        q = self.result_list.aggregate(tomato_sum=Sum('num_in_box'))
        self.tomato_count = q['tomato_sum']

class TomatoAdmin(admin.ModelAdmin):

    def get_changelist(self, request):
        return MyChangeList

    class Meta:
        model = CherryTomato

    list_display = ('name', 'num_in_box')

admin.site.register(CherryTomato, TomatoAdmin)

change_list.html (relevant bit only, copy/paste the existing one and extend it)

  {% block result_list %}
      {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %}
      {% result_list cl %}
      {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %}
      Tomatoes on this page: {{ cl.tomato_count }}
  {% endblock %}

I'll leave it up to you how to style this the way you want it.

c4urself
  • 4,207
  • 21
  • 32
  • Actually this is better solution, because customising ChangeList as above can aggregate "total" with respect to selected filters. – pista329 Dec 10 '14 at 21:54
  • Better and can get along with filters :) – elsadek Dec 21 '14 at 21:32
  • This is near perfect for me. The total is reflecting the pagination as well since that does go into the query set. – teewuane Jan 13 '16 at 21:31
  • I'm getting a template error: 'Invalid block tag on line 4: 'admin_actions', expected 'elif', 'else' or 'endif'. Did you forget to register or load this tag?' I don't understand the endif is there. – L. P. Oct 15 '20 at 18:24
  • just checked, that is still the correct name for it (see: https://github.com/django/django/blob/master/django/contrib/admin/templates/admin/change_list.html). you may not be overriding it correctly due to an incorrect template name? – c4urself Oct 15 '20 at 18:51
18

This thread is going on for a while, but I'm probably not the last one that is looking for such a feature. Therefore, I created a package that makes it easy to add totals.

Checkout https://github.com/douwevandermeij/admin-totals

Usage:

from admin_totals.admin import ModelAdminTotals
from django.contrib import admin
from django.db.models import Sum, Avg

@admin.register(MyModel)
class MyModelAdmin(ModelAdminTotals):
    list_display = ['col_a', 'col_b', 'col_c']
    list_totals = [('col_b', Sum), ('col_c', Avg)]

Feel free to fork it and improve it.

Douwe van der Meij
  • 1,062
  • 12
  • 7
7

Ok, so there is a way to do this, involving adding in a couple of new template tags and extending the admin templates.

First off, in your app’s templatetags folder, you create an admin_totals.py file containing a template tag to create a totals row:

from django.template import Library

register = Library()

def totals_row(cl):
    total_functions = getattr(cl.model_admin, 'total_functions', {})
    totals = []
    for field_name in cl.list_display:
        if field_name in total_functions:
            values = [getattr(i, field_name) for i in cl.result_list]
            totals.append(total_functions[field_name](values))
        else:
            totals.append('')
    return {'cl': cl, 'totals_row': totals}
totals_row = register.inclusion_tag("myapp/totals_row.html")(totals_row)

Then you need the template for said row in myapp/totals_row.html (wherever your templates are):

<table id="result_totals">
    <tfoot>
        <tr>
            {% for total in totals_row %}<td>{{ total }}</td>{% endfor %}
        </tr>
    </tfoot>
</table>

Then you need to wire that into a custom admin template inheriting from Django’s default, such as myapp/mymodel_admin.html:

{% extends "admin/change_list.html" %}

{% load admin_totals %}

{% block result_list %}
{{ block.super }}
{% totals_row cl %}
{% endblock %}

Finally, you wire that into the configuration in your admin.py file in your app:

class MyModelAdmin(ModelAdmin):

    list_display = ('name', 'date', 'numerical_awesomeness')
    total_functions = {'numerical_awesomeness': sum}

    change_list_template = 'myapp/mymodel_admin.html'

That should wire in the custom admin template for your new model, displaying the totals row. You can also extend it with other summary functions other than sum, should you so wish.

One small point left: the totals row isn’t actually in the results table, because that would require some fairly gnarly copying and pasting of the Django admin templates. For bonus points, you can add in the following smidge of JavaScript to the bottom of your totals_row.html file:

<script type="text/javascript">
    django.jQuery('#result_list').append(django.jQuery('#result_totals tfoot')[0])
    django.jQuery('#result_totals').remove()
</script>

One caveat: all this will only reflect the totals for the items currently displayed, rather than for all items in existence. One way around this is to set list_per_page to some infeasibly large number on your ModelAdmin class, if you don’t mind the potential performance hit.

dhwthompson
  • 2,501
  • 1
  • 15
  • 11
4

There is also the package "django-admin-changelist-stats" that can give summary statistics. I have successfully used it in a Django 1.6 project.

IMO use c4urself's solution for fine-grained template customization, but if its only aggregates (sum/avg/min/max) of tables you are after then this solution is pretty good.

From its website

" This simple application provides stats and aggregation capabilities for the Django admin changelist view. It allows to display stats at the end of the changelist page in a easy way, just adding one option to the model admin object. "

More information,including examples, at its bitbucket repository webpage https://bitbucket.org/frankban/django-admin-changelist-stats/src

Sachin
  • 915
  • 6
  • 10
3

To calculate by applying the filters:

def changelist_view(self, request, extra_context=None):
    extra_context = extra_context or {}
    extra_context['total'] = sum([item.cash for item in self.get_queryset(request)])
    return super().changelist_view(request, extra_context=extra_context)

def get_queryset(self, request):
    qs = super().get_queryset(request)
    return qs.filter(**request.GET.dict())
2

In the original question it was clarified in a comment that they are interested in all the objects on the current page. If you are instead interested in all the objects in the database, but with filters applied, you could take a route similar to c4urself's answer, but with the following core bit:

from django.contrib import admin
from django.db.models import Sum

from tomatoes.tomato.models import CherryTomato

class TomatoAdmin(admin.ModelAdmin):

    def get_changelist_instance(self, request):
        cl = super().get_changelist_instance(request)
        q = cl.get_queryset(request).aggregate(tomato_sum=Sum('num_in_box'))
        cl.tomato_count = q['tomato_sum']
        return cl

    class Meta:
        model = CherryTomato

    list_display = ('name', 'num_in_box')
Klaas van Schelven
  • 2,374
  • 1
  • 21
  • 35
0

if you create a custom view overiding the admin view you should be able to take the queryset along with the current page information and slice the data to create a total appropriate to the current page. if you want a true total than I still see a custom overridden admin view as the option you are looking for.

Dave LeBlanc
  • 251
  • 2
  • 7
-3

AFAIK there's no way to do this without creating a custom view. The concept is a little flawed anyway, because a user would expect such a row to show the total for only the visible objects, rather than all the objects in the queryset. Thus, any pagination would result in confusion.

As an aside, you can add extra columns to an admin list view by doing something like the following:

class ItemAdmin(admin.ModelAdmin):
    model = Item

    list_display = ('field1', 'field2', 'extra_field')

    def extra_field(self, obj):
        return u'%s' % do_something_with(obj)
Evan Brumley
  • 2,468
  • 20
  • 13