modified MultipleChartViews to instantiate only one chart.

added chart on index view
This commit is contained in:
artus40
2017-08-19 18:00:49 +02:00
parent 6d2ce7d7bc
commit 0bf2d9ff79
9 changed files with 319 additions and 301 deletions

View File

@@ -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,35 +78,36 @@ 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,}},
},
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),
)
@@ -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

View File

@@ -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()])

View File

@@ -3,5 +3,6 @@
<h4>Période</h4>
<form action="" method="get">
{% bootstrap_form form layout="inline" %}
<input type="hidden" name="graph" value="{{active}}" />
{% bootstrap_button "Ok" button_type="submit" %}
</form>

View File

@@ -1,2 +1,4 @@
<div class="text-center">
<div id="{{ chart.get_html_id }}" style="width: {{ chart.width }}px; height: {{ chart.height }}px;"></div>
<a class="btn btn-sm btn-default" href="#" id="image-{{ chart.get_html_id }}"><span class="glyphicon glyphicon-save-file"></span> Télécharger l'image</a>
</div>

View File

@@ -6,6 +6,7 @@
{% block sidebar %}
{{ block.super }}
<hr />
<div class="panel panel-primary">
<div class="panel-body text-right">
{% include "statistiques/filter_form.html" %}
@@ -19,22 +20,5 @@
{% endblock %}
{% block page_content %}
<div class="col-lg-8 text-right">
<h3 class="page-header">Données générales</h3>
<table class="table table-bordered">
<tr><th>...</th><th>Maraudes</th><th>Nombre de rencontres <span class="badge">moyenne par maraude</span></th><th>Personnes</th></tr>
<tr><th>Total</th><td>{{nbr_maraudes}}</td><td>{{nbr_rencontres}} <span class="badge">{{nbr_rencontres_moyenne }}</span></td><td>{{nbr_sujets}}</td></tr>
<tr><th>Soirée</th><td>{{nbr_maraudes_nuit}}</td><td>{{nbr_rencontres_nuit}} <span class="badge">{{nbr_rencontres_nuit_moyenne }}</span></td><td>{{nbr_sujets_nuit}}</td></tr>
<tr><th>Journée</th><td>{{nbr_maraudes_jour}}</td><td>{{nbr_rencontres_jour}} <span class="badge">{{nbr_rencontres_jour_moyenne }}</span></td><td>{{nbr_sujets_jour}}</td></tr>
</table>
</div>
<div class="col-lg-4">
<div class="alert alert-info">
<p>Voici les données permettant une analyse statistiques des maraudes.</p>
<p>Vous pouvez sélectionner une période particulière ou l'ensemble des données</p>
<p>Les données sont réparties en trois catégories, accessibles par le menu sur la gauche</p>
</div>
</div>
{{ chart.as_html }}
{% endblock %}

View File

@@ -1,19 +1,18 @@
{% load navbar %}
<ul class="nav nav-pills nav-stacked text-right">
<li role="presentation" {% active namespace="statistiques" viewname="index" %}>
<a href="{% url "statistiques:index" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Maraudes&nbsp;
<a href="{% url "statistiques:index" %}?period={{year|default:0}}">Maraudes&nbsp;
<span class="glyphicon glyphicon-road"></span>
</a>
</li>
<li role="presentation" {% active namespace="statistiques" viewname="pies" %}>
<a href="{% url "statistiques:pies" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Typologie&nbsp;
<li role="presentation" {% active namespace="statistiques" viewname="typologie" %}>
<a href="{% url "statistiques:typologie" %}?period={{year|default:0}}">Typologie&nbsp;
<span class="glyphicon glyphicon-user"></span>
</a>
</li>
<li role="presentation" {% active namespace="statistiques" viewname="frequentation" %}>
<a href="{% url "statistiques:frequentation" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Fréquentation&nbsp;
<a href="{% url "statistiques:frequentation" %}?period={{year|default:0}}">Fréquentation&nbsp;
<span class="glyphicon glyphicon-stats"></span>
</a>
</li>
</ul>
<hr />

View File

@@ -2,42 +2,37 @@
{% block title %}{{block.super}} {{page_title}}{% endblock %}
{% block breadcrumbs %}{{block.super}}<li>{{page_title}}</li>{% endblock %}
{% block breadcrumbs %}
{{block.super}}
<li>{{page_title}}</li>
{% if active %}<li>{{active}}</li>{% endif %}
{% endblock %}
{% block sidebar %}
{{ block.super }}
{% if chart %}
<hr />
<div class="panel panel-primary">
<div class="panel-body text-right">
{% include "statistiques/filter_form.html" %}
{% include "statistiques/filter_form.html" with active=active %}
<hr />
<p>Échantillon : {{ queryset_count }} objets</p>
</div>
</div>
{% endif %}
{% endblock %}
{% block page_content %}
<script type="text/javascript">
function hideAll() {
{% for _, graph in graphs %}{% with graph.get_html_id as id %}
$("#tab-{{id}}").attr("class", "");
$("#wrapper-{{id}}").hide();
{% endwith %}{% endfor %}
}
function showGraph(id) {
hideAll();
$("#tab-" + id).attr("class", "active");
$("#wrapper-" + id).show();
}
</script>
<ul class="nav nav-tabs">
{% for title, graph in graphs %}<li role="presentation" id="tab-{{graph.get_html_id}}"><a href="#" onclick="showGraph('{{graph.get_html_id}}');">{{ title }}</a></li>{% endfor %}
<ul class="nav nav-pills nav-justified">
{% for name in chart_list %}
<li role="presentation" {%if name == active%} class="active" {%endif%}>
<a href="?graph={{name}}&period={{year}}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% for title, graph in graphs %}
<div class="row text-center" id="wrapper-{{ graph.get_html_id}}">
{{ graph.as_html }}
</div>
{% endfor %}
{% if chart %}
{{ chart.as_html }}
{% endif %}
{% endblock %}

View File

@@ -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<pk>[0-9]+)/$', views.StatistiquesDetailsView.as_view(), name="details"),
url(r'^update/(?P<pk>[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"),
]

View File

@@ -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)
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['page_title'] = str(self.page_title)
context['queryset_count'] = queryset.count()
context['graphs'] = [
(title, graph) for title, graph in self.get_graphs(queryset)
]
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
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)),
)
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()
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