modified MultipleChartViews to instantiate only one chart.
added chart on index view
This commit is contained in:
@@ -1,65 +1,44 @@
|
||||
import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.shortcuts import redirect
|
||||
from django.views import generic
|
||||
from django.db.models import (CharField, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from django.db.models.functions.datetime import ExtractMonth
|
||||
from graphos.sources.simple import SimpleDataSource
|
||||
from graphos.renderers import gchart
|
||||
|
||||
from .models import FicheStatistique
|
||||
from .forms import StatistiquesForm, SelectRangeForm
|
||||
from .charts import PieWrapper, ColumnWrapper
|
||||
from django.db.models import (CharField, NullBooleanField)
|
||||
|
||||
from maraudes.notes import Observation
|
||||
from maraudes.models import Maraude, HORAIRES_APRESMIDI, HORAIRES_SOIREE
|
||||
from notes.models import Sujet
|
||||
|
||||
from .models import FicheStatistique
|
||||
from .forms import StatistiquesForm, SelectRangeForm
|
||||
from . import charts
|
||||
###
|
||||
|
||||
NO_DATA = "Aucune donnée"
|
||||
NOM_MOIS = {
|
||||
1: "Janvier",
|
||||
2: "Février",
|
||||
3: "Mars",
|
||||
4: "Avril",
|
||||
5: "Mai",
|
||||
6: "Juin",
|
||||
7: "Juillet",
|
||||
8: "Août",
|
||||
9: "Septembre",
|
||||
10: "Octobre",
|
||||
11: "Novembre",
|
||||
12: "Décembre"
|
||||
}
|
||||
|
||||
|
||||
class FilterMixin(generic.edit.FormMixin):
|
||||
class FilterMixin(generic.edit.FormView):
|
||||
|
||||
form_class = SelectRangeForm
|
||||
request = None
|
||||
year = None
|
||||
month = None
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'month': self.request.GET.get('month', 0),
|
||||
'year': self.request.GET.get('year', 0),
|
||||
'period': self.year,
|
||||
}
|
||||
|
||||
def parse_args(self, request):
|
||||
self.year = int(request.GET.get('year', 0))
|
||||
self.month = int(request.GET.get('month', 0))
|
||||
period = request.GET.get("period", "0")
|
||||
self.year = int(period)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.parse_args(request)
|
||||
return super().get(self, *args, **kwargs)
|
||||
|
||||
def _filters(self, prefix):
|
||||
return {'%s__%s' % (prefix, attr): getattr(self, attr) for attr in ('year', 'month')
|
||||
if getattr(self, attr) > 0 }
|
||||
return {'%s__%s' % (prefix, attr): getattr(self, attr) for attr in ('year',)
|
||||
if getattr(self, attr) > 0}
|
||||
|
||||
def get_observations_queryset(self):
|
||||
return Observation.objects.filter(**self._filters('created_date'))
|
||||
@@ -68,7 +47,7 @@ class FilterMixin(generic.edit.FormMixin):
|
||||
return Maraude.objects.filter(**self._filters('date'))
|
||||
|
||||
def get_fichestatistiques_queryset(self):
|
||||
return FicheStatistique.objects.filter(pk__in=self.get_observations_queryset().values_list('sujet'))
|
||||
return FicheStatistique.objects.filter(pk__in=self.get_sujets_queryset().values_list('pk'))
|
||||
|
||||
def get_sujets_queryset(self, selection=None):
|
||||
if not selection:
|
||||
@@ -78,29 +57,43 @@ class FilterMixin(generic.edit.FormMixin):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['year'] = self.year
|
||||
context['month'] = self.month
|
||||
return context
|
||||
|
||||
|
||||
class MultipleChartsView(FilterMixin, generic.TemplateView):
|
||||
|
||||
title = None
|
||||
description = None
|
||||
_charts = {} # Set of charts managed by the view
|
||||
|
||||
_active = None # Name of the active chart
|
||||
_chart = None # Active chart object
|
||||
template_name = "statistiques/multiple_charts.html"
|
||||
page_title = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not self._charts:
|
||||
raise ImproperlyConfigured()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._active = request.GET.get('graph', None)
|
||||
if self._active:
|
||||
self._chart = self._charts.get(self._active, None)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
""" Returns the queryset of objects used to draw graphs """
|
||||
raise NotImplementedError("Subclass must implement this method")
|
||||
|
||||
def get_graphs(self, queryset):
|
||||
raise NotImplementedError("Subclasses must implement this method.")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
queryset = self.get_queryset()
|
||||
context['page_title'] = str(self.page_title)
|
||||
context['queryset_count'] = queryset.count()
|
||||
context['graphs'] = [
|
||||
(title, graph) for title, graph in self.get_graphs(queryset)
|
||||
]
|
||||
context['page_title'] = str(self.title)
|
||||
context['chart_list'] = list(self._charts.keys())
|
||||
context['active'] = self._active
|
||||
if self._chart: # Need to instantiate the chart
|
||||
queryset = self.get_queryset()
|
||||
context['queryset_count'] = queryset.count()
|
||||
context['chart'] = self._chart(queryset)
|
||||
return context
|
||||
|
||||
|
||||
@@ -113,196 +106,49 @@ class DashboardView(FilterMixin, generic.TemplateView):
|
||||
maraudes = self.get_maraudes_queryset()
|
||||
rencontres = self.get_observations_queryset()
|
||||
|
||||
context['nbr_maraudes'] = maraudes.count() or NO_DATA
|
||||
context['nbr_maraudes_jour'] = maraudes.filter(
|
||||
heure_debut=HORAIRES_APRESMIDI
|
||||
).count() or NO_DATA
|
||||
context['nbr_maraudes_nuit'] = maraudes.filter(
|
||||
heure_debut=HORAIRES_SOIREE
|
||||
).count() or NO_DATA
|
||||
|
||||
context['nbr_rencontres'] = rencontres.count() or NO_DATA
|
||||
rencontres_jour = rencontres.filter(
|
||||
rencontre__maraude__heure_debut=HORAIRES_APRESMIDI
|
||||
)
|
||||
rencontres_nuit = rencontres.filter(
|
||||
rencontre__maraude__heure_debut=HORAIRES_SOIREE
|
||||
)
|
||||
context['nbr_rencontres_jour'] = rencontres_jour.count() or NO_DATA
|
||||
context['nbr_rencontres_nuit'] = rencontres_nuit.count() or NO_DATA
|
||||
|
||||
for r, m in [
|
||||
('nbr_rencontres', 'nbr_maraudes'),
|
||||
('nbr_rencontres_nuit', 'nbr_maraudes_nuit'),
|
||||
('nbr_rencontres_jour', 'nbr_maraudes_jour'),
|
||||
]:
|
||||
try:
|
||||
context['%s_moyenne' % r] = int(context[r] / context[m])
|
||||
except (ZeroDivisionError, TypeError):
|
||||
context['%s_moyenne' % r] = NO_DATA
|
||||
|
||||
context['nbr_sujets'] = self.get_sujets_queryset(selection=rencontres).count()
|
||||
context['nbr_sujets_jour'] = self.get_sujets_queryset(selection=rencontres_jour).count()
|
||||
context['nbr_sujets_nuit'] = self.get_sujets_queryset(selection=rencontres_nuit).count()
|
||||
context['chart'] = charts.DonneeGeneraleChart(
|
||||
maraudes=(maraudes,
|
||||
maraudes.filter(heure_debut=HORAIRES_APRESMIDI),
|
||||
maraudes.filter(heure_debut=HORAIRES_SOIREE)
|
||||
),
|
||||
rencontres=(rencontres,
|
||||
rencontres.filter(rencontre__maraude__heure_debut=HORAIRES_APRESMIDI),
|
||||
rencontres.filter(rencontre__maraude__heure_debut=HORAIRES_SOIREE)),
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class PieChartView(MultipleChartsView):
|
||||
page_title = "Typologie"
|
||||
class TypologieChartsView(MultipleChartsView):
|
||||
title = "Typologie"
|
||||
|
||||
_charts = OrderedDict(
|
||||
[('Âge', charts.AgePieChart),
|
||||
('Genre', charts.GenrePieChart),
|
||||
] +
|
||||
[(f.verbose_name, charts.generate_piechart_for_field(f)) for f in FicheStatistique._meta.get_fields()
|
||||
if f.__class__ in (NullBooleanField, CharField)
|
||||
]
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_fichestatistiques_queryset()
|
||||
|
||||
def get_graphs(self, queryset):
|
||||
sujets = self.get_sujets_queryset()
|
||||
# Insertion des champs 'âge' et 'genre' du modèle notes.Sujet
|
||||
for field in Sujet._meta.fields:
|
||||
if field.name == 'genre':
|
||||
yield str(field.verbose_name), PieWrapper(sujets, field)
|
||||
if field.name == 'age':
|
||||
categories = (
|
||||
('Mineurs', range(0,18)),
|
||||
('18-24', range(18,25)),
|
||||
('25-34', range(25,35)),
|
||||
('35-44', range(35,45)),
|
||||
('45-54', range(45,55)),
|
||||
('+ de 55', range(55,110)),
|
||||
)
|
||||
nbr_sujets = lambda rg: sujets.filter(age__in=rg).count()
|
||||
|
||||
yield "Âge", PieWrapper(
|
||||
data=[("age", "count")] +
|
||||
[(label, nbr_sujets(rg))
|
||||
for label, rg in categories] +
|
||||
[("Ne sait pas", sujets.filter(age=None).count())],
|
||||
title="Âge des sujets")
|
||||
|
||||
# Puis des champs du modèle statistiques.FicheStatistique
|
||||
# dans leur ordre de déclaration
|
||||
for field in FicheStatistique._meta.fields:
|
||||
if field.__class__ in (NullBooleanField, CharField):
|
||||
yield str(field.verbose_name), PieWrapper(queryset, field)
|
||||
|
||||
|
||||
class FrequentationStatsView(MultipleChartsView):
|
||||
page_title = "Fréquentation"
|
||||
|
||||
@staticmethod
|
||||
def calculer_frequentation_par_quart_heure(observations, continu=False):
|
||||
""" Calcule le nombre d'observations, de 16h à 24h, par tranche de 15min.
|
||||
L'algorithme est *très peu* efficace mais simple à comprendre : on calcule pour
|
||||
chaque tranche les observations qui y sont contenues.
|
||||
On peut calculer seulement les observations démarrées (continu = False) ou considérer
|
||||
que l'observation est contenue dans un intervalle sur toute sa durée (continu = True).
|
||||
|
||||
"""
|
||||
data = dict()
|
||||
|
||||
def genere_filtre_pour(heure, indice):
|
||||
""" Renvoie une fonction qui renvoie True si l'intervalle donné contient l'observation, c'est-à-dire :
|
||||
1. Elle démarre/finit dans l'intervalle.
|
||||
2. Elle démarre avant et fini après l'intervalle.
|
||||
"""
|
||||
debut_intervalle = indice * 15
|
||||
fin_intervalle = debut_intervalle + 15
|
||||
rng = range(debut_intervalle, fin_intervalle)
|
||||
|
||||
def est_contenue(observation):
|
||||
""" Vérifie l'observation est contenue dans l'intervalle """
|
||||
debut = datetime.datetime.strptime(
|
||||
"%s" % observation.rencontre.heure_debut,
|
||||
"%H:%M:%S"
|
||||
)
|
||||
fin = debut + datetime.timedelta(0, observation.rencontre.duree * 60)
|
||||
|
||||
# L'observation démarre dans l'intervalle
|
||||
if (debut.hour == heure and debut.minute in rng):
|
||||
return True
|
||||
# L'observation finit dans l'intervalle, seulement si continu est True
|
||||
elif continu and (fin.hour == heure and fin.minute in rng):
|
||||
return True
|
||||
# L'observation démarre avant ET finit après l'intervalle,
|
||||
# seulement si continu est True
|
||||
elif ( continu
|
||||
and (debut.hour <= heure and debut.minute <= debut_intervalle)
|
||||
and (fin.hour >= heure and fin.minute >= fin_intervalle)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return est_contenue
|
||||
|
||||
for h in range(16, 24):
|
||||
for i in range(4):
|
||||
|
||||
filtre = genere_filtre_pour(heure=h, indice=i)
|
||||
contenus = list(filter(filtre, observations))
|
||||
|
||||
key = datetime.time(h, i * 15)
|
||||
data[key] = len(contenus)
|
||||
|
||||
return data
|
||||
class FrequentationChartsView(MultipleChartsView):
|
||||
title = "Fréquentation"
|
||||
_charts = OrderedDict([
|
||||
('Par mois', charts.RencontreParMoisChart),
|
||||
('Par heure', charts.RencontreParHeureChart),
|
||||
('Par sujet', charts.RencontreParSujetChart),
|
||||
])
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_observations_queryset()
|
||||
|
||||
def get_graphs(self, queryset):
|
||||
graphs = []
|
||||
# Nombre de rencontres en fonction de l'heure
|
||||
par_heure = self.calculer_frequentation_par_quart_heure(queryset, continu=False)
|
||||
en_continu = self.calculer_frequentation_par_quart_heure(queryset, continu=True)
|
||||
graphs.append(("Par heure", gchart.AreaChart(
|
||||
SimpleDataSource(
|
||||
[("Heure", "Rencontres démarrées", "Au total (démarré + en cours)")] +
|
||||
[(heure, par_heure[heure], en_continu[heure]) for heure in sorted(par_heure.keys())]
|
||||
),
|
||||
options={
|
||||
"title": "Fréquentation de la maraude en fonction de l'heure (par quart d'heure)"
|
||||
}
|
||||
)))
|
||||
|
||||
# Nombre de rencontres en fonction du mois
|
||||
par_mois = queryset.annotate(
|
||||
mois=ExtractMonth('created_date')
|
||||
).values(
|
||||
'mois'
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
).order_by()
|
||||
graphs.append(("Par mois", ColumnWrapper(
|
||||
SimpleDataSource(
|
||||
[("Mois", "Rencontres")] +
|
||||
[(NOM_MOIS[item['mois']], item['nbr']) for item in par_mois]
|
||||
),
|
||||
options={
|
||||
"title": "Nombre de rencontres par mois"
|
||||
}
|
||||
)))
|
||||
|
||||
# Graph: Fréquence de rencontres par sujet
|
||||
nbr_rencontres = queryset.values('sujet').annotate(nbr=Count('pk')).order_by()
|
||||
categories = (
|
||||
('Rencontre unique', (1,)),
|
||||
('Entre 2 et 5 rencontres', range(2, 6)),
|
||||
('Entre 6 et 20 rencontres', range(6, 20)),
|
||||
('Plus de 20 rencontres', range(20, 999)),
|
||||
)
|
||||
get_count_for_range = lambda rg: nbr_rencontres.filter(nbr__in=rg).count()
|
||||
graphs.append(("Par sujet", PieWrapper(
|
||||
data=[('Type de rencontre', 'Nombre de sujets')] +
|
||||
[(label, get_count_for_range(rg)) for label, rg in categories],
|
||||
title='Fréquence de rencontres'
|
||||
)))
|
||||
|
||||
# Rencontres par lieu
|
||||
# TODO: More customizable way of categorizing "Lieu"
|
||||
|
||||
return graphs
|
||||
|
||||
|
||||
# AjaxMixin
|
||||
class AjaxOrRedirectMixin:
|
||||
class AjaxOrRedirectMixin(generic.DetailView):
|
||||
""" For view that should be retrieved by Ajax only. If not,
|
||||
redirects to the primary view where these are displayed """
|
||||
|
||||
@@ -313,13 +159,13 @@ class AjaxOrRedirectMixin:
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
||||
class StatistiquesDetailsView(AjaxOrRedirectMixin):
|
||||
|
||||
model = FicheStatistique
|
||||
template_name = "statistiques/fiche_stats_details.html"
|
||||
|
||||
|
||||
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
||||
class StatistiquesUpdateView(AjaxOrRedirectMixin):
|
||||
|
||||
model = FicheStatistique
|
||||
form_class = StatistiquesForm
|
||||
|
||||
Reference in New Issue
Block a user