Skip to content

Instantly share code, notes, and snippets.

@erickgnavar
Forked from hakib/admin.py
Created March 14, 2019 01:25
Show Gist options
  • Save erickgnavar/eedbe9ca2a28e7fd98707b40ef6e89c7 to your computer and use it in GitHub Desktop.
Save erickgnavar/eedbe9ca2a28e7fd98707b40ef6e89c7 to your computer and use it in GitHub Desktop.
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 %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment