feat: add person stats to dashboard

This commit is contained in:
relikd
2023-06-10 19:33:45 +02:00
parent 231433b65e
commit ccfc0518e3
4 changed files with 106 additions and 23 deletions

View File

@@ -269,6 +269,13 @@ table.clickable tbody>tr:hover {
.text-green { .text-green {
color: #23be58; color: #23be58;
} }
.text-gray {
color: #aaa;
}
#by_month_history td:first-of-type {
text-decoration: dotted underline;
}
#profileMenu { #profileMenu {
position: absolute; position: absolute;

View File

@@ -3,6 +3,18 @@
{% block content %} {% block content %}
<h3>Personen</h3>
<p>
Personen gesamt: <b>{{person.by_count.total}}</b>
(davon haben <b>{{person.by_count.single_visit}}</b> die Werkstatt nur 1x besucht
und <b>{{person.by_count.long_not_seen}}</b> waren seit über einem Jahr nicht mehr da)
</p>
<p>
<b>{{person.by_count.no_booking}}</b> haben noch nie Werkstattzeit gebucht.
<b>{{person.by_count.no_course}}</b> haben nie eine Einweisung gemacht.
</p>
<h3>Nach Art</h3>
<div class="div-by-side"> <div class="div-by-side">
<div> <div>
@@ -42,23 +54,41 @@
</div> </div>
<h3>{{ head.bookings }}</h3> <h3>{{ head.bookings }}</h3>
<table class="table table-sm"> <table id="by_month_history" class="table table-sm">
{% for year, stat in booking.by_month.items reversed %} {% for year, stat in by_month %}
<tr class="thead"> <tr class="thead">
<th><h4 class="mb-0">{{ year }}</h4></th> <th><h4 class="mb-0">{{ year }}</h4></th>
<th></th><th>Jan</th><th>Feb</th><th>Mär</th><th>Apr</th><th>Mai</th><th>Jun</th><th>Jul</th><th>Aug</th><th>Sep</th><th>Okt</th><th>Nov</th><th>Dez</th> <th></th><th>Jan</th><th>Feb</th><th>Mär</th><th>Apr</th><th>Mai</th><th>Jun</th><th>Jul</th><th>Aug</th><th>Sep</th><th>Okt</th><th>Nov</th><th>Dez</th>
</tr> </tr>
<tr>
<th>Personen</th>
{% for sum, count, people in stat %}
<td{% if not people %} class="text-gray"{% endif %}>
{% if people and not forloop.first %}
<a href="{% url 'person:list' %}?created.y={{year}}&created.m={{forloop.counter0}}">{{ people }}</a>
{% else %}
{{ people }}
{% endif %}
</td>
{% endfor %}
</tr>
<tr> <tr>
<th>Buchungen</th> <th>Buchungen</th>
{% for sum, count in stat %}<td>{{ count }}</td>{% endfor %} {% for sum, count, people in stat %}
<td{% if not count %} class="text-gray"{% endif %}>{{ count }}</td>
{% endfor %}
</tr> </tr>
<tr> <tr>
<th>Minuten (ø)</th> <th>Minuten (ø)</th>
{% for sum, count in stat %}<td>{{ sum|divide:count|floatformat:1 }}</td>{% endfor %} {% for sum, count, people in stat %}
<td{% if not sum %} class="text-gray"{% endif %}>{{ sum|divide:count|floatformat:1 }}</td>
{% endfor %}
</tr> </tr>
<tr> <tr>
<th>Minuten</th> <th>Minuten</th>
{% for sum, count in stat %}<td>{{ sum }}</td>{% endfor %} {% for sum, count, people in stat %}
<td{% if not sum %} class="text-gray"{% endif %}>{{ sum }}</td>
{% endfor %}
</tr> </tr>
<tr class="blank"></tr> <tr class="blank"></tr>
{% endfor %} {% endfor %}

View File

@@ -1,8 +1,11 @@
from django.db.models import Q, F, Sum, Count, ExpressionWrapper, IntegerField from django.db.models import Q, F, Sum, Count, ExpressionWrapper, IntegerField
from django.db.models.functions import Substr from django.db.models.functions import Substr
from django.db.models.lookups import LessThan
from django.views.generic import TemplateView from django.views.generic import TemplateView
from app.base.models import Booking, BookingType, Trait, TraitMapping from app.base.models import (
Booking, BookingType, CourseVisit, Person, Trait, TraitMapping
)
from app.base.views.login import LoginRequired from app.base.views.login import LoginRequired
from app.base.views.model_views.base import ViewOptions from app.base.views.model_views.base import ViewOptions
@@ -23,30 +26,20 @@ class DashboardView(DashboardOptions, TemplateView):
book_types = dict(BookingType.objects.values_list('key', 'label')) book_types = dict(BookingType.objects.values_list('key', 'label'))
trait_types = dict(Trait.objects.values_list('key', 'label')) trait_types = dict(Trait.objects.values_list('key', 'label'))
# Booking stats
book_by_type = stats_for_booking('type')
book_by_month = {}
for stat in stats_for_booking('month'):
year, month = stat['month'].split('-')
if year not in book_by_month:
book_by_month[year] = [[0, 0] for x in range(13)]
book_by_month[year][0][0] += stat['sum']
book_by_month[year][0][1] += stat['count']
book_by_month[year][int(month)][0] += stat['sum']
book_by_month[year][int(month)][1] += stat['count']
context['booking'] = { context['booking'] = {
'labels': book_types, 'labels': book_types,
'by_type': book_by_type, 'by_type': stats_for_booking('type'),
'by_month': book_by_month,
} }
context['trait'] = { context['trait'] = {
'labels': trait_types, 'labels': trait_types,
'by_type': stats_for_traits(), 'by_type': stats_for_traits(),
} }
context['person'] = {
'by_count': stats_for_person_count(),
}
context['by_month'] = stats_by_month()
context['head'] = { context['head'] = {
'person': Person._meta.verbose_name_plural,
'bookings': Booking._meta.verbose_name_plural, 'bookings': Booking._meta.verbose_name_plural,
'booking_types': BookingType._meta.verbose_name_plural, 'booking_types': BookingType._meta.verbose_name_plural,
'traits': Trait._meta.verbose_name_plural, 'traits': Trait._meta.verbose_name_plural,
@@ -55,9 +48,59 @@ class DashboardView(DashboardOptions, TemplateView):
return context return context
def stats_for_person_count():
today = date.today()
last_year = today.replace(year=today.year - 1)
stats = Person.objects.aggregate(
total=Count(1),
single_visit=Sum(LessThan(
# TODO: other DBs dont use microseconds
ExpressionWrapper(F('last_visit') - F('created'),
output_field=IntegerField()),
3 * 86_400_000_000)), # 3 days
long_not_seen=Sum(Q(last_visit__lt=last_year)),
)
stats['no_booking'] = stats['total'] - Booking.objects.aggregate(
c=Count('user', distinct=True))['c']
stats['no_course'] = stats['total'] - CourseVisit.objects.aggregate(
c=Count('participant', distinct=True))['c']
return stats
def stats_by_month():
stats = {}
for stat in stats_for_person_by_month():
year, month = stat['month'].split('-')
if year not in stats:
stats[year] = [[0, 0, 0] for x in range(13)]
stats[year][0][2] += stat['count']
stats[year][int(month)][2] += stat['count']
for stat in stats_for_booking('month'):
year, month = stat['month'].split('-')
if year not in stats:
stats[year] = [[0, 0, 0] for x in range(13)]
stats[year][0][0] += stat['sum']
stats[year][0][1] += stat['count']
stats[year][int(month)][0] += stat['sum']
stats[year][int(month)][1] += stat['count']
return sorted(stats.items(), reverse=True)
def stats_for_person_by_month():
return Person.objects.values(
month=Substr('created', 1, 7), # YYYY-MM
).annotate(count=Count(1)).order_by('month')
def stats_for_booking(groupby: str): def stats_for_booking(groupby: str):
_Q = Booking.objects.filter(Q(end_time__isnull=False)).annotate( _Q = Booking.objects.filter(Q(end_time__isnull=False)).annotate(
# for whatever reason django uses nano seconds # TODO: other DBs dont use microseconds
diff=ExpressionWrapper( diff=ExpressionWrapper(
(F('end_time') - F('begin_time')) / 60_000_000, (F('end_time') - F('begin_time')) / 60_000_000,
output_field=IntegerField()), output_field=IntegerField()),

View File

@@ -33,6 +33,9 @@ class PersonOptions(ViewOptions[Person], LoginRequired):
list_filter = { list_filter = {
'trait': 'traits__pk', 'trait': 'traits__pk',
'course': 'courses__pk', 'course': 'courses__pk',
'created': 'created',
'created.y': 'created__year',
'created.m': 'created__month',
} }
list_columns = ['display_name', 'birth_date', 'last_visit'] list_columns = ['display_name', 'birth_date', 'last_visit']
list_render = { list_render = {