diff --git a/statistiques/charts.py b/statistiques/charts.py
index dd5e41e..e3f5b51 100644
--- a/statistiques/charts.py
+++ b/statistiques/charts.py
@@ -1,22 +1,48 @@
+import datetime
+
+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 notes.models import Sujet
+
+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"},
+ NullBooleanField: {True: "Oui", False: "Non", None: "Ne sait pas"},
}
+# TODO: Retrieve charts data from cache to avoid recalculating on each request
+class CachedDataSource:
+ pass
+
+# TODO: Clean up...
+
+
class FieldValuesCountDataSource(SimpleDataSource):
""" Generates data from a limited set of choices.
"""
- def __init__(self, queryset, field, labels=None, excluded=[]):
+ def __init__(self, queryset, field, labels=None, excluded=None):
self.queryset = queryset
self.field_name = field.name
- self.excluded = excluded
+ self.excluded = excluded or []
if not labels:
if field.__class__ in LABELS:
labels = LABELS[field.__class__]
@@ -52,38 +78,39 @@ class PieWrapper(gchart.PieChart):
- a data object and title
"""
- height=400
- width=800
+ 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=[],
+ null_values=None,
**kwargs):
- if not data:
+ if data is None:
if not isinstance(field, Field):
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
- if not queryset:
- raise TypeError("You must give either a queryset and field or data")
data_source = FieldValuesCountDataSource(
queryset, field,
excluded=null_values,
- labels=None #TODO: How to pass in labels ??
+ labels=None # TODO: How to pass in labels ??
)
else:
data_source = SimpleDataSource(data=data)
- super().__init__(
- data_source,
- options={
- 'title': getattr(field, 'verbose_name', title),
- 'is3D': True,
- 'pieSliceText': 'value',
- 'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16,}},
- },
- width=kwargs.get('width', self.width),
- height=kwargs.get('height', self.height),
- )
+ 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"
@@ -94,4 +121,176 @@ class PieWrapper(gchart.PieChart):
class ColumnWrapper(gchart.ColumnChart):
- pass
+ options = {
+ 'is3D': False,
+ 'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16, }},
+ 'title': 'test',
+ }
+
+ def __init__(self, *args, **kwargs):
+ kwargs.update(options=self.options.copy())
+ super().__init__(*args, **kwargs)
+
+
+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 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(gchart.ColumnChart):
+
+ 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"
+ }
+ )
+
+
+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_par_quart_heure(queryset, continu=False)
+ en_continu = self.calculer_frequentation_par_quart_heure(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_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
diff --git a/statistiques/forms.py b/statistiques/forms.py
index 705d0bd..9fe2fc1 100644
--- a/statistiques/forms.py
+++ b/statistiques/forms.py
@@ -32,11 +32,4 @@ def get_year_range():
class SelectRangeForm(forms.Form):
- year = forms.ChoiceField(label="Année", choices=[(0, 'Toutes')] + [(i, str(i)) for i in get_year_range()])
- month = forms.ChoiceField(label="Mois",
- choices=[(0, 'Tous'),
- (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')
- ],
- )
+ period = forms.ChoiceField(label="Année", choices=[(0, 'Tout')] + [(i, str(i)) for i in get_year_range()])
diff --git a/statistiques/templates/statistiques/filter_form.html b/statistiques/templates/statistiques/filter_form.html
index 64a5b85..b02a816 100644
--- a/statistiques/templates/statistiques/filter_form.html
+++ b/statistiques/templates/statistiques/filter_form.html
@@ -3,5 +3,6 @@
Période
diff --git a/statistiques/templates/statistiques/gchart/html.html b/statistiques/templates/statistiques/gchart/html.html
index 158556d..098eb7e 100644
--- a/statistiques/templates/statistiques/gchart/html.html
+++ b/statistiques/templates/statistiques/gchart/html.html
@@ -1,2 +1,4 @@
+
diff --git a/statistiques/templates/statistiques/index.html b/statistiques/templates/statistiques/index.html
index 9750cff..6773786 100644
--- a/statistiques/templates/statistiques/index.html
+++ b/statistiques/templates/statistiques/index.html
@@ -6,6 +6,7 @@
{% block sidebar %}
{{ block.super }}
+
{% include "statistiques/filter_form.html" %}
@@ -19,22 +20,5 @@
{% endblock %}
{% block page_content %}
-
-
-
-
-
- | ... | Maraudes | Nombre de rencontres moyenne par maraude | Personnes |
- | Total | {{nbr_maraudes}} | {{nbr_rencontres}} {{nbr_rencontres_moyenne }} | {{nbr_sujets}} |
- | Soirée | {{nbr_maraudes_nuit}} | {{nbr_rencontres_nuit}} {{nbr_rencontres_nuit_moyenne }} | {{nbr_sujets_nuit}} |
- | Journée | {{nbr_maraudes_jour}} | {{nbr_rencontres_jour}} {{nbr_rencontres_jour_moyenne }} | {{nbr_sujets_jour}} |
-
-
-
-
-
Voici les données permettant une analyse statistiques des maraudes.
-
Vous pouvez sélectionner une période particulière ou l'ensemble des données
-
Les données sont réparties en trois catégories, accessibles par le menu sur la gauche
-
-
+ {{ chart.as_html }}
{% endblock %}
diff --git a/statistiques/templates/statistiques/menu.html b/statistiques/templates/statistiques/menu.html
index 136769a..17fbcf9 100644
--- a/statistiques/templates/statistiques/menu.html
+++ b/statistiques/templates/statistiques/menu.html
@@ -1,19 +1,18 @@
{% load navbar %}
-
diff --git a/statistiques/templates/statistiques/multiple_charts.html b/statistiques/templates/statistiques/multiple_charts.html
index c8c3293..e12180d 100644
--- a/statistiques/templates/statistiques/multiple_charts.html
+++ b/statistiques/templates/statistiques/multiple_charts.html
@@ -2,42 +2,37 @@
{% block title %}{{block.super}} {{page_title}}{% endblock %}
-{% block breadcrumbs %}{{block.super}}
{{page_title}}{% endblock %}
+{% block breadcrumbs %}
+ {{block.super}}
+
{{page_title}}
+ {% if active %}
{{active}}{% endif %}
+{% endblock %}
{% block sidebar %}
{{ block.super }}
+ {% if chart %}
+
- {% include "statistiques/filter_form.html" %}
+ {% include "statistiques/filter_form.html" with active=active %}
Échantillon : {{ queryset_count }} objets
+ {% endif %}
{% endblock %}
{% block page_content %}
-
-
- {% for title, graph in graphs %}- {{ title }}
{% endfor %}
+
+ {% for name in chart_list %}
+ -
+ {{ name }}
+
+ {% endfor %}
- {% for title, graph in graphs %}
-
- {{ graph.as_html }}
-
- {% endfor %}
+ {% if chart %}
+ {{ chart.as_html }}
+ {% endif %}
{% endblock %}
diff --git a/statistiques/urls.py b/statistiques/urls.py
index 2983a80..62a627a 100644
--- a/statistiques/urls.py
+++ b/statistiques/urls.py
@@ -4,9 +4,8 @@ from . import views
urlpatterns = [
url('^$', views.DashboardView.as_view(), name="index"),
- url('^charts/$', views.PieChartView.as_view(), name="pies"),
+ url('^charts/$', views.TypologieChartsView.as_view(), name="typologie"),
url(r'^details/(?P[0-9]+)/$', views.StatistiquesDetailsView.as_view(), name="details"),
url(r'^update/(?P[0-9]+)/$', views.StatistiquesUpdateView.as_view(), name="update"),
- url(r'^frequentation/$', views.FrequentationStatsView.as_view(), name="frequentation"),
+ url(r'^frequentation/$', views.FrequentationChartsView.as_view(), name="frequentation"),
]
-
diff --git a/statistiques/views.py b/statistiques/views.py
index 7fcbff3..feaf8a6 100644
--- a/statistiques/views.py
+++ b/statistiques/views.py
@@ -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