Blog Archive

Monday, 16 January 2012

Django - Adding row to the admin list with the totals and averages of given fields

Once a while a wild ticked occurred for one of the projects I was developing.
The issue was that they needed a rows with the totals and averages to be displayed at the end of the table in the admin changelist view for particular fields.
So here how it works






from django.contrib.admin.views.main import ChangeList
from django.db.models import Sum, Avg
 
from myapp.models import MyModelName
 
class TotalAveragesChangeList(ChangeList):
    #provide the list of fields that we need to calculate averages and totals
    fields_to_total = ['amount', 'total_sum', 'paid_by_cash',
                       'paid_by_transfer',]
 
 
 
    def get_total_values(self, queryset):
        """
        Get the totals
        """
        #basically the total parameter is an empty instance of the given model
        total =  MyModelName()
        total.custom_alias_name = "Totals" #the label for the totals row
        for field in self.fields_to_total:
            setattr(total, field, queryset.aggregate(Sum(field)).items()[0][1])
        return total
 
 
    def get_average_values(self, queryset):
        """
        Get the averages
        """
        average =  MyModelName()
        average.custom_alias_name = "Averages" #the label for the averages row
 
        for field in self.fields_to_total:
            setattr(average, field, queryset.aggregate(Avg(field)).items()[0][1])
        return average
 
 
    def get_results(self, request):
        """
        The model admin gets queryset results from this method
        and then displays it in the template
        """
        super(TotalAveragesChangeList, self).get_results(request)
        #first get the totals from the current changelist
        total = self.get_total_values(self.query_set)
        #then get the averages
        average = self.get_average_values(self.query_set)
        #small hack. in order to get the objects loaded we need to call for 
        #queryset results once so simple len function does it
        len(self.result_list)
        #and finally we add our custom rows to the resulting changelist
        self.result_list._result_cache.append(total)
        self.result_list._result_cache.append(average)

And all we need is just to set our custom changelist to the AdminModel


class MyModelNameAdmin(admin.ModelAdmin):
 
    def get_changelist(self, request, **kwargs):
        return TotalAveragesChangeList
 
admin.site.register(MyModelName, MyModelNameAdmin)

10 comments:

  1. Thanks a ton! I was looking for a way to figure this out

    ReplyDelete
  2. Thanks AzMan, very usefull code. Could you post the code for?:
    - where you use custom_alias_name as label.
    - where you set the CSS or clases for row background colors

    Other question: The admin.py file is the correct place to define the TotalAveragesChangeList class?

    Thanks a lot

    ReplyDelete
    Replies
    1. You can place it there or take out into any outside module. I would place it into a different file, out of admin.
      I have overriden built in admin css file using static files.

      Delete
  3. Hi Azamat,

    May I ask question?
    Where does total.custom_alias_name = "Totals" #the label for the totals row come from?

    I am getting the value correct but without label in the row.

    Thnaks for your blog

    Peter

    ReplyDelete
    Replies
    1. Same question... the answer dont work for me...

      Delete
  4. You need to add custom_alias_name property in your model.


    For example like this

    def __unicode__(self):
    if self.title:
    return self.title
    else:
    return self.custom_alias_name

    It has to be defined in your model class
    So what happens here is that when it is an instance of model that is not loaded from db it will return the custom_alias_name instead of title

    ReplyDelete
    Replies
    1. Cheers Azamat,
      You have given me hint,

      Keep on writing more articles.


      Peter

      Delete
  5. In my case the subtotal is a calculated field (read_only). How to use in your method on the fields_to_total?

    #models.py
    class DetVenda(models.Model):
    quantity = models.IntegerField()
    price = models.DecimalField(max_digits=8, decimal_places=2)

    def _get_subtotal(self):
    if self.quantity:
    return self.price * self.quantity
    subtotal = property(_get_subtotal)

    #admin.py
    class TotalChangeList(ChangeList):
    fields_to_total = ['subtotal']

    def get_total_values(self, queryset):
    total = Sale()
    total.custom_alias_name = "Totals"
    for field in self.fields_to_total:
    setattr(
    total, field, queryset.aggregate(Sum(field)).items()[0][1])
    return total

    def get_results(self, request):
    super(TotalChangeList, self).get_results(request)
    total = self.get_total_values(self.query_set)
    len(self.result_list)
    self.result_list._result_cache.append(total)

    FieldError at /admin/Sale:

    Cannot resolve keyword 'total' into field. Choices are:

    How to resolve this?

    ReplyDelete