125 lines
4.1 KiB
Python
Executable File
125 lines
4.1 KiB
Python
Executable File
from django.db.models import Q, F, Sum, Count, ExpressionWrapper, IntegerField
|
|
from django.db.models.functions import Substr
|
|
from django.db.models.lookups import LessThan
|
|
from django.views.generic import TemplateView
|
|
|
|
from app.base.models import (
|
|
Booking, BookingType, CourseVisit, Person, Trait, TraitMapping
|
|
)
|
|
from app.base.views.login import LoginRequired
|
|
from app.base.views.model_views.base import ViewOptions
|
|
|
|
from datetime import date
|
|
from typing import Any
|
|
|
|
|
|
class DashboardOptions(ViewOptions, LoginRequired):
|
|
icon = 'chart-line'
|
|
title = 'Dashboard'
|
|
|
|
|
|
class DashboardView(DashboardOptions, TemplateView):
|
|
template_name = 'dashboard.html'
|
|
|
|
def get_context_data(self, **kwargs) -> 'dict[str, Any]':
|
|
context = super().get_context_data(**kwargs)
|
|
book_types = dict(BookingType.objects.values_list('key', 'label'))
|
|
trait_types = dict(Trait.objects.values_list('key', 'label'))
|
|
|
|
context['booking'] = {
|
|
'labels': book_types,
|
|
'by_type': stats_for_booking('type'),
|
|
}
|
|
context['trait'] = {
|
|
'labels': trait_types,
|
|
'by_type': stats_for_traits(),
|
|
}
|
|
context['person'] = {
|
|
'by_count': stats_for_person_count(),
|
|
}
|
|
context['by_month'] = stats_by_month()
|
|
context['head'] = {
|
|
'person': Person._meta.verbose_name_plural,
|
|
'bookings': Booking._meta.verbose_name_plural,
|
|
'booking_types': BookingType._meta.verbose_name_plural,
|
|
'traits': Trait._meta.verbose_name_plural,
|
|
}
|
|
|
|
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):
|
|
_Q = Booking.objects.filter(Q(end_time__isnull=False)).annotate(
|
|
# TODO: other DBs dont use microseconds
|
|
diff=ExpressionWrapper(
|
|
(F('end_time') - F('begin_time')) / 60_000_000,
|
|
output_field=IntegerField()),
|
|
).filter(diff__lt=1440) # filter 24+ hours. Probably wrong checkout
|
|
|
|
if groupby == 'month':
|
|
_Q = _Q.annotate(month=Substr('begin_time', 1, 7)) # YYYY-MM
|
|
|
|
return _Q.values(groupby).annotate(
|
|
sum=Sum('diff', output_field=IntegerField()),
|
|
count=Count(1),
|
|
).values(groupby, 'sum', 'count').order_by(groupby)
|
|
|
|
|
|
def stats_for_traits():
|
|
return TraitMapping.objects.filter(
|
|
Q(valid_from__lte=date.today()),
|
|
Q(valid_until=None) | Q(valid_until__gte=date.today()),
|
|
).values('trait').annotate(
|
|
count=Count(1),
|
|
).values('trait', 'count').order_by('trait')
|