-
-
Save erickgnavar/eedbe9ca2a28e7fd98707b40ef6e89c7 to your computer and use it in GitHub Desktop.
How to Turn Django Admin Into a Lightweight Dashboard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% 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