330 lines
11 KiB
Python
330 lines
11 KiB
Python
import datetime
|
|
import collections
|
|
|
|
from django.db.models.functions.datetime import ExtractMonth
|
|
from django.db.models import (Field, NullBooleanField,
|
|
Count,
|
|
)
|
|
from graphos.sources.simple import SimpleDataSource
|
|
from graphos.renderers import gchart
|
|
|
|
from maraudes.models import Rencontre
|
|
from notes.models import Sujet
|
|
from .models import GroupeLieux
|
|
|
|
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"
|
|
}
|
|
# Defines generic labels for common fields
|
|
LABELS = {
|
|
NullBooleanField: {True: "Oui", False: "Non", None: "Ne sait pas"},
|
|
}
|
|
|
|
|
|
class FieldValuesCountDataSource(SimpleDataSource):
|
|
""" Generates data from a limited set of choices.
|
|
|
|
"""
|
|
def __init__(self, queryset, field, labels=None, excluded=None):
|
|
self.queryset = queryset
|
|
self.field_name = field.name
|
|
self.excluded = excluded or []
|
|
if not labels:
|
|
if field.__class__ in LABELS:
|
|
labels = LABELS[field.__class__]
|
|
elif field.choices:
|
|
labels = dict(field.choices)
|
|
else:
|
|
raise ValueError("Could not retrieve labels for", field)
|
|
self.labels = labels
|
|
super().__init__(self.create_data())
|
|
|
|
def create_data(self):
|
|
data = [(self.field_name, "%s_count" % self.field_name)] # Headers
|
|
data += [
|
|
(self.labels[item[self.field_name]], # Display a label instead of raw values
|
|
item['count']
|
|
) for item in self.queryset.values( # Retrieve all values for field
|
|
self.field_name
|
|
).annotate( # Count occurrences of each value
|
|
count=Count('pk')
|
|
).order_by() # Needed so that counts are aggregated
|
|
# Exclude values that are marked to be ignored
|
|
if (not self.excluded
|
|
or item[self.field_name] not in self.excluded)
|
|
]
|
|
return data
|
|
|
|
|
|
class PieWrapper(gchart.PieChart):
|
|
""" A wrapper around gchart.PieChart that generates a graph from :
|
|
|
|
- a queryset and a model field (NullBooleanField or field with choices)
|
|
OR
|
|
- a data object and title
|
|
"""
|
|
|
|
options = {
|
|
'is3D': False,
|
|
'pieSliceText': 'value',
|
|
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16, }},
|
|
}
|
|
height = 400
|
|
width = 800
|
|
|
|
def __init__(self,
|
|
queryset=None, field=None,
|
|
data=None, title=None,
|
|
null_values=None,
|
|
**kwargs):
|
|
if data is None:
|
|
if not isinstance(field, Field):
|
|
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
|
|
data_source = FieldValuesCountDataSource(
|
|
queryset, field,
|
|
excluded=null_values,
|
|
labels=None # TODO: How to pass in labels ??
|
|
)
|
|
else:
|
|
data_source = SimpleDataSource(data=data)
|
|
|
|
options = self.options.copy()
|
|
options.update(
|
|
title=getattr(field, 'verbose_name', title)
|
|
)
|
|
super().__init__(data_source,
|
|
options=options,
|
|
width=kwargs.get('width', self.width),
|
|
height=kwargs.get('height', self.height),
|
|
)
|
|
|
|
def get_js_template(self):
|
|
return "statistiques/gchart/pie_chart.html"
|
|
|
|
def get_html_template(self):
|
|
return "statistiques/gchart/html.html"
|
|
|
|
|
|
class ColumnWrapper(gchart.ColumnChart):
|
|
|
|
options = {
|
|
'is3D': False,
|
|
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16, }},
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.update(self.options.copy())
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def get_js_template(self):
|
|
return "statistiques/gchart/column_chart.html"
|
|
|
|
def get_html_template(self):
|
|
return "statistiques/gchart/html.html"
|
|
|
|
|
|
class DonneeGeneraleChart(gchart.BarChart):
|
|
|
|
def __init__(self, maraudes=None, rencontres=None, sujets=None):
|
|
|
|
data = [("...", "Soirée", "Journée", {'role': 'annotation'})]
|
|
|
|
data += [("Maraudes", maraudes[2].count(), maraudes[1].count(), maraudes[0].count())]
|
|
data += [("Rencontres", rencontres[2].count(), rencontres[1].count(), rencontres[0].count())]
|
|
if sujets:
|
|
data += [("Nouvelles rencontres", sujets[2].count(), sujets[1].count(), sujets[0].count())]
|
|
|
|
super().__init__(SimpleDataSource(data), options={'title': 'Données générales', 'isStacked': 'percent'})
|
|
|
|
|
|
def generate_piechart_for_field(field):
|
|
""" Returns a PieWrapper subclass working with a fixed field """
|
|
class FieldChart(PieWrapper):
|
|
def __init__(self, queryset):
|
|
super().__init__(queryset, field)
|
|
return FieldChart
|
|
|
|
|
|
class GenrePieChart(PieWrapper):
|
|
def __init__(self, queryset):
|
|
queryset = Sujet.objects.filter(pk__in=queryset.values_list('pk'))
|
|
super().__init__(queryset, Sujet._meta.get_field('genre'))
|
|
|
|
|
|
class AgePieChart(PieWrapper):
|
|
""" Chart for 'age' field of Sujet model """
|
|
|
|
labels = (('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)),
|
|
)
|
|
|
|
def count_nbr_sujets(self, age_rng):
|
|
return self.queryset.filter(age__in=age_rng).count()
|
|
|
|
def __init__(self, queryset):
|
|
data = [("age", "count")]
|
|
if queryset:
|
|
self.queryset = Sujet.objects.filter(pk__in=queryset.values_list('pk'))
|
|
data += [(label, self.count_nbr_sujets(rg)) for label, rg in self.labels]
|
|
data += [("Ne sait pas", self.queryset.filter(age=None).count())]
|
|
super().__init__(data=data, title="Âge")
|
|
|
|
|
|
class IndividuGroupeChart(PieWrapper):
|
|
|
|
def __init__(self, queryset):
|
|
data = [("individu/groupe", "count")]
|
|
|
|
# Cast parent Rencontre objects
|
|
queryset = Rencontre.objects.filter(
|
|
pk__in=queryset.values_list('rencontre')
|
|
)
|
|
|
|
counts = collections.defaultdict(lambda: 0)
|
|
for rencontre in queryset:
|
|
counts[rencontre.groupe_ou_individu()] += 1
|
|
print(counts)
|
|
|
|
for label, count in counts.items():
|
|
data += [(label, count)]
|
|
super().__init__(data=data, title="Individu ou Groupe")
|
|
|
|
|
|
class RencontreParSujetChart(PieWrapper):
|
|
|
|
labels = (('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)),
|
|
)
|
|
|
|
def get_count_for_range(self, rng):
|
|
return self.queryset.filter(nbr__in=rng).count()
|
|
|
|
def __init__(self, queryset):
|
|
data = [('Type de rencontre', 'Nombre de sujets')]
|
|
if queryset:
|
|
self.queryset = queryset.values('sujet').annotate(nbr=Count('pk')).order_by()
|
|
data += [(label, self.get_count_for_range(rg)) for label, rg in self.labels]
|
|
super().__init__(data=data,
|
|
title='Fréquence de rencontres'
|
|
)
|
|
|
|
|
|
class RencontreParMoisChart(ColumnWrapper):
|
|
|
|
def __init__(self, queryset):
|
|
data = [("Mois", "Rencontres")]
|
|
if queryset:
|
|
par_mois = queryset.annotate(
|
|
mois=ExtractMonth('created_date')
|
|
).values(
|
|
'mois'
|
|
).annotate(
|
|
nbr=Count('pk')
|
|
).order_by()
|
|
data += [(NOM_MOIS[item['mois']], item['nbr']) for item in par_mois]
|
|
else:
|
|
data += [("Mois", 0)]
|
|
|
|
super().__init__(SimpleDataSource(data),
|
|
options={
|
|
"title": "Nombre de rencontres par mois"
|
|
}
|
|
)
|
|
|
|
|
|
def gen_heure_minute(_from, to):
|
|
start_hour = _from.hour
|
|
stop_hour, stop_min = to.hour, to.minute
|
|
|
|
for hour in range(start_hour, stop_hour + 1):
|
|
yield datetime.time(hour, 0)
|
|
if not (hour == stop_hour and 30 > stop_min):
|
|
yield datetime.time(hour, 30)
|
|
|
|
|
|
class RencontreParHeureChart(gchart.AreaChart):
|
|
def __init__(self, queryset):
|
|
data = [("Heure", "Rencontres démarrées", "Au total (démarré + en cours)")]
|
|
if queryset:
|
|
par_heure = self.calculer_frequentation(queryset, continu=False)
|
|
en_continu = self.calculer_frequentation(queryset, continu=True)
|
|
data += [(heure, par_heure[heure], en_continu[heure]) for heure in sorted(par_heure.keys())]
|
|
else:
|
|
data += [("Heure", 0, 0)]
|
|
|
|
super().__init__(SimpleDataSource(data),
|
|
options={
|
|
"title": "Fréquentation de la maraude en fonction de l'heure (par quart d'heure)",
|
|
}
|
|
)
|
|
|
|
@staticmethod
|
|
def calculer_frequentation(observations, step=15, continu=False):
|
|
|
|
def find_intervalle(temps):
|
|
return datetime.time(temps.hour, temps.minute // step * step)
|
|
|
|
def range_over_intervalles(rencontre):
|
|
""" Génère tous les intervalles contenus entre le début et la fin de la rencontre """
|
|
curseur = find_intervalle(rencontre.heure_debut)
|
|
# Convertir en objet datetime
|
|
curseur = datetime.datetime.now().replace(hour=curseur.hour,
|
|
minute=curseur.minute)
|
|
debut = datetime.datetime.now().replace(hour=rencontre.heure_debut.hour,
|
|
minute=rencontre.heure_debut.minute)
|
|
fin = debut + datetime.timedelta(0, rencontre.duree * 60)
|
|
delta = datetime.timedelta(0, step * 60)
|
|
|
|
while curseur < fin:
|
|
yield datetime.time(curseur.hour, curseur.minute)
|
|
curseur += delta
|
|
|
|
data = collections.defaultdict(lambda: 0)
|
|
|
|
for rencontre in map(lambda o: o.rencontre, observations):
|
|
if not continu:
|
|
data[find_intervalle(rencontre.heure_debut)] += 1
|
|
else:
|
|
for intervalle in range_over_intervalles(rencontre):
|
|
data[intervalle] += 1
|
|
|
|
return data
|
|
|
|
|
|
class RencontreParLieuChart(PieWrapper):
|
|
|
|
@property
|
|
def labels(self):
|
|
for groupe_lieux in GroupeLieux.objects.all():
|
|
yield (groupe_lieux.label,
|
|
tuple(groupe_lieux.lieux.values_list('pk', flat=True))
|
|
)
|
|
|
|
def get_count_for_group(self, lieu_pks):
|
|
return self.queryset.filter(rencontre__lieu__pk__in=lieu_pks).count()
|
|
|
|
def __init__(self, queryset):
|
|
self.queryset = queryset
|
|
data = [('Lieu de rencontre', 'Nombre de rencontres')]
|
|
if self.queryset:
|
|
data += [(label, self.get_count_for_group(lieu_pks)) for label, lieu_pks in self.labels]
|
|
super().__init__(data=data,
|
|
title="Fréquentation par lieu")
|