Skip to content

Instantly share code, notes, and snippets.

@hakib hakib/admin.py

Last active Aug 9, 2020
Embed
What would you like to do?
How to Turn Django Admin Into a Lightweight Dashboard
# https://hakibenita.com/how-to-turn-django-admin-into-a-lightweight-dashboard
from django.contrib import admin
from django.db.models import Count, Sum, Min, Max, DateTimeField
from django.db.models.functions import Trunc
from . import models
def get_next_in_date_hierarchy(request, date_hierarchy):
if date_hierarchy + '__day' in request.GET:
return 'hour'
if date_hierarchy + '__month' in request.GET:
return 'day'
if date_hierarchy + '__year' in request.GET:
return 'week'
return 'month'
@admin.register(models.SaleSummary)
class LoadContractSummaryAdmin(admin.ModelAdmin):
change_list_template = 'admin/dashboard/sales_change_list.html'
actions = None
date_hierarchy = 'created'
# Prevent additional queries for pagination.
show_full_result_count = False
list_filter = (
'device',
)
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return True
def has_module_permission(self, request):
return True
def changelist_view(self, request, extra_context=None):
response = super().changelist_view(request, extra_context=extra_context)
# self.get_queryset would return the base queryset. ChangeList
# apply the filters from the request so this is the only way to
# get the filtered queryset.
try:
qs = response.context_data['cl'].queryset
except (AttributeError, KeyError):
# See issue #172.
# When an invalid filter is used django will redirect. In this
# case the response is an http redirect response and so it has
# no context_data.
return response
# List view
metrics = {
'total': Count('id'),
'total_sales': Sum('price'),
}
response.context_data['summary'] = list(
qs
.values('sale__category__name')
.annotate(**metrics)
.order_by('-total_sales')
)
# List view summary
response.context_data['summary_total'] = dict(qs.aggregate(**metrics))
# Chart
period = get_next_in_date_hierarchy(request, self.date_hierarchy)
response.context_data['period'] = period
summary_over_time = qs.annotate(
period=Trunc('created', 'day', output_field=DateTimeField()),
).values('period')
.annotate(total=Sum('price'))
.order_by('period')
summary_range = summary_over_time.aggregate(
low=Min('total'),
high=Max('total'),
)
high = summary_range.get('high', 0)
low = summary_range.get('low', 0)
response.context_data['summary_over_time'] = [{
'period': x['period'],
'total': x['total'] or 0,
'pct': \
((x['total'] or 0) - low) / (high - low) * 100
if high > low else 0,
} for x in summary_over_time]
return response
# https://hakibenita.com/how-to-turn-django-admin-into-a-lightweight-dashboard
from app.models import Sale
class SaleSummary(Sale):
class Meta:
proxy = True
verbose_name = 'Sale Summary'
verbose_name_plural = 'Sales Summary'
{% extends "admin/change_list.html" %}
{% load i18n %}
{% load humanize %}
{% load mathtags %}
{% load tz %}
{% block content_title %}
<h1> {% trans 'Sales Summary' %} </h1>
{% endblock %}
{% block result_list %}
<div class="results">
<table>
<thead>
<tr>
<th> <div class="text"> <a href="#">Category </a> </div> </th>
<th> <div class="text"> <a href="#">Total </a> </div> </th>
<th> <div class="text"> <a href="#">Total Sales </a> </div> </th>
<th> <div class="text"> <a href="#"><strong>% Of Total Sales</strong></a> </div> </th>
</tr>
</thead>
<tbody>
{% for row in summary %}
<tr class="{% cycle 'row1' 'row2' %}">
<td> {{ row.category }} </td>
<td> {{ row.total }} </td>
<td> {{ row.total_sales | default:0 }} </td>
<td><strong> {{ row.total_sales | default:0 | percentof:summary_total.total_sales }} </strong> </td>
</tr>
{% endfor %}
<tr style="font-weight:bold; border-top:2px solid #DDDDDD;">
<td> Total </td>
<td> {{ summary_total.total | intcomma }} </td>
<td> {{ summary_total.total_sales | default:0 }} </td>
<td> 100% </td>
</tr>
</tbody>
</table>
</div>
<h2> {% blocktrans %} Sales time (by {{ period}}) {% endblocktrans %} </h2>
<style>
.bar-chart {
height: 160px;
padding-top: 60px;
display: flex;
justify-content: space-around;
overflow: hidden;
}
.bar-chart .bar {
background-color: #79aec8;
flex: 100%;
align-self: flex-end;
margin-right: 2px;
position: relative;
}
.bar-chart .bar:last-child {
margin: 0;
}
.bar-chart .bar:hover {
background-color: #417690;
}
.bar-chart .bar .bar-tooltip {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
position: relative;
z-index: 999;
}
.bar-chart .bar .bar-tooltip {
position: absolute;
top: -60px;
left: 50%;
transform: translateX(-50%);
text-align: center;
font-weight: bold;
opacity: 0;
}
.bar-chart .bar:first-child .bar-tooltip {
transform: initial;
text-align: initial;
left: 0;
}
.bar-chart .bar:last-child .bar-tooltip {
transform: initial;
text-align: right;
right: 0;
left: initial;
}
.bar-chart .bar:hover .bar-tooltip {
opacity: 1;
}
</style>
{% timezone 'UTC' %}
<div class="results">
<div class="bar-chart">
{% for x in summary_over_time %}
<div class="bar" style="height:{{x.pct}}%">
<div class="bar-tooltip">
{{x.total }}<br>
{{x.period | date:"d/m/Y H:i"}}
</div>
</div>
{% endfor %}
</div>
</div>
{% endtimezone %}
{% endblock %}
{% block pagination %}{% endblock %}
@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Dec 23, 2018

This is the source code for the article How to Turn Django Admin Into a Lightweight Dashboard

@dannluciano

This comment has been minimized.

Copy link

dannluciano commented Sep 2, 2019

Thanks Man! I love with your blog.
😍

@dannluciano

This comment has been minimized.

Copy link

dannluciano commented Sep 2, 2019

In the end of 4-th line has a not necessary closed parentheses

@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Sep 3, 2019

Thanks Man! I love with your blog.

That's awesome!

In the end of 4-th line has a not necessary closed parentheses

Which file?

@thinmy

This comment has been minimized.

Copy link

thinmy commented Sep 13, 2019

admin.py

@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Sep 14, 2019

Thanks, updated!

@Naxaes

This comment has been minimized.

Copy link

Naxaes commented Oct 3, 2019

This is great! Is this free to use and modify for commercial use or is there a license attached to this?

@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Oct 3, 2019

Hey @Naxaes, you can use this snippet. Be sure to buy me a beer after your first 1M$...

@Naxaes

This comment has been minimized.

Copy link

Naxaes commented Oct 3, 2019

It's a deal! :D

@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Dec 8, 2019

Hey @OmarGonD,
I'm not sure I understand your question. In your model admin you override changelist_view, so calling super() execute the function in ModelAdmin.

@OmarGonD

This comment has been minimized.

Copy link

OmarGonD commented Dec 8, 2019

@habik I've manage to solved my last question. But I'm having trouble rendering the percent that a particular sale represents of total.

I'm pasting my code, so hopefully you can take a look:

image

Code:

models.py:

class Order(models.Model):
    ORDER_STATUS = (
        ('recibido_pagado', 'Recibido y pagado'),
        ('recibido_no_pagado', 'Recibido pero no pagado'),
        ('en_proceso', 'En proceso'),
        ('en_camino', 'En camino'),
        ('entregado', 'Entregado'),
        ('cancelado', 'Cancelado por no pagar' )
    )
    token = models.CharField(max_length=100, blank=True, null=True)
    first_name = models.CharField(max_length=50, blank=True, null=True)
    last_name = models.CharField(max_length=50, blank=True, null=True)
    phone_number = models.CharField(max_length=30, blank=True)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    stickers_price = models.DecimalField(max_digits=10, decimal_places=2)
    discount = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
    shipping_cost = models.DecimalField(max_digits=10, decimal_places=2)
    email = models.EmailField(max_length=250, blank = True, verbose_name= 'Correo electrónico')
    last_four = models.CharField(max_length=100, blank=True, null=True)
    created = models.DateTimeField(auto_now_add=True)
    shipping_address = models.CharField(max_length=100, blank=True, null=True)
    shipping_address1 = models.CharField(max_length=100, blank=True, null=True)
    reference = models.CharField(max_length=100, blank=True, null=True)
    shipping_department = models.CharField(max_length=100, blank=True, null=True)
    shipping_province = models.CharField(max_length=100, blank=True, null=True)
    shipping_district = models.CharField(max_length=100, blank=True, null=True)
    reason = models.CharField(max_length=400, blank=True, null=True, default='')
    status = models.CharField(max_length=20, choices=ORDER_STATUS, default='recibido_pagado')
    comments = models.CharField(max_length=400, blank=True, null=True, default='')
    cupon = models.ForeignKey('marketing.Cupons', blank=True, null=True, default=None, on_delete=models.SET_NULL)

class OrderSummary(Order): #Extends funcs of model without creating a table in DB
    class Meta:
        proxy = True #important A proxy model extends the functionality of another model without creating an actual table in the database
        verbose_name = 'Order Summary'
        verbose_name_plural = 'Orders Summary'

admin.py:



@admin.register(OrderSummary)
class OrderSummaryAdmin(admin.ModelAdmin):
    change_list_template = 'admin/order_summary_change_list.html'
    date_hierarchy = 'created'

    def changelist_view(self, request, extra_context=None):
        response = super().changelist_view(
            request,
            extra_context=extra_context,
        )
        try:
            qs = response.context_data['cl'].queryset
        except (AttributeError, KeyError):
            return response
        
        metrics = {
            'num': Count('id'),
            'total_sales': Sum('total'),
            'total_shipping_cost': Sum('shipping_cost'),
            'total_no_shipping_cost': Sum(F('total') - F('shipping_cost')),
            'percent_of_total': Sum(F('total') - F('shipping_cost')/ F('total')),
        }

        response.context_data['summary'] = list(
            qs
            .values('id','total', 'shipping_cost')
            .annotate(**metrics)
            .order_by('-created')
        )

        response.context_data['summary_total'] = dict(
            qs.aggregate(**metrics)
        )
       
        return response

template.html:

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

{% block content_title %}
    <h1> Sales Summary </h1>
{% endblock %}
{% block result_list %}
<div class=”results”>
    <table>
        
    <thead>
      <tr>
        <th>
          <div class=”text”>
            <a href=”#”># ÓRDENES</a>
          </div>
        </th>
        <th>
          <div class=”text”>
            <a href=”#”>TOTAL VENTAS</a>
          </div>
        </th>
        <th>
            <div class=”text”>
              <a href=”#”>
                <strong>TOTAL NO SHIPPING</strong>
              </a>
            </div>
          </th>
        <th>
          <div class=”text”>
            <a href=”#”>
              <strong>SHIPPING COST</strong>
            </a>
          </div>
        </th>
        <th>
                <div class=”text”>
                  <a href=”#”>
                    <strong>PORCENTAJE DEL TOTAL</strong>
                  </a>
                </div>
              </th>
      </tr>
    </thead>
    <tbody>
        {% for row in summary %}
        <tr class="{% cycle 'row1' 'row2' %}">
          <td> {{ row.id }}  </td>
          <td> {{ row.total | intcomma }} </td>
          <td> S/ {{ row.total_no_shipping_cost | default:0 | intcomma }} </td>
          <td> S/ {{ row.shipping_cost | default:0 | intcomma }} </td>
          <td>
              <strong>
            {{ row.total | 
              default:0 | 
              percentof:row.total }} 
          </strong>
          </td>
        </tr>

        {% endfor %}
        <tr style="font-weight:bold; border-top:2px solid #DDDDDD;">
            <td> # {{ summary_total.num | intcomma }} </td>
            <td> S/ {{ summary_total.total_sales | intcomma }} </td>
            <td> S/ {{ summary_total.total_no_shipping_cost | default:0 }} </td>
            <td> S/ {{ summary_total.total_shipping_cost | default:0 }} </td>
            <td>  
                <strong>
                100%
                </strong> 
            </td>
        </tr>
      </tbody>

      
    
  </table>
</div>
{% endblock %}
{% block pagination %}{% endblock %}
@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Dec 8, 2019

My guess is that you are unable render the percent because you put the expression in multiple lines:

{{ row.total | 
              default:0 | 
              percentof:row.total }} 

Also, the templatetag percentof is not a built-in, you need to implement it your self.

@mascDriver

This comment has been minimized.

Copy link

mascDriver commented Jan 28, 2020

my self.date_hierarchy + '__month' in request.GET return always false, could you post your model that defines the 'created'?

@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Jan 28, 2020

You need to set date_hierarchy on the model admin (as the example above shows) to a date field on your model + you need the actually apply the date hierarchy filter for it to return True.

@YuhoChen

This comment has been minimized.

Copy link

YuhoChen commented Apr 12, 2020

{% load mathtags %} is error

@hakib

This comment has been minimized.

Copy link
Owner Author

hakib commented Apr 12, 2020

That's right @yuhao,

You need to create it and register it yourself:

from django import template

register = template.Library()


@register.filter()
def divide(n1, n2):
    try:
        return n1 / n2
    except (ZeroDivisionError, TypeError):
        return None


@register.filter()
def floor_divide(n1, n2):
    try:
        return n1 // n2
    except (ZeroDivisionError, TypeError):
        return None


@register.filter()
def multiply(n1, n2):
    try:
        return n1 * n2
    except TypeError:
        return None
@YuhoChen

This comment has been minimized.

Copy link

YuhoChen commented Apr 12, 2020

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.