* setup new 'statistiques' module * added 'graphos' package and created first test graph * put graphos in requirements, deleted local folder * added "load_csv" management command ! * added update of premiere_rencontre field in 'load_csv' management command * added missing urls.py file * added 'merge' action and view * added 'info_completed' ratio * linked sujets:merge views inside suivi:details * added link to maraudes:details in notes table headers, if any * Major reorganisation, moved 'suivi' and 'sujets' to 'notes', cleanup in 'maraudes', dropping 'website' mixins (mostly useless) * small cleanup * worked on Maraude and Sujet lists * corrected missing line in notes.__init__ * restored 'details' view for maraudes and sujets insie 'notes' module * worked on 'notes': added navigation between maraude's compte-rendu, right content in details, header to list tables * changed queryset for CompteRenduDetailsView to all notes of same date, minor layout changes * added right content to 'details-sujet', created 'statistiques' view and update templates * restored 'statistiques' ajax view in 'details-sujet', fixed 'merge_two' util function * added auto-creation of FicheStatistique (plus some tests), pagination for notes in 'details-sujet' * added error-prone cases in paginator * fixed non-working modals, added titles * added UpdateStatistiques capacity in CompteRenduCreate view * fixed missing AjaxTemplateMixin for CreateSujetView, worked on compte-rendu creation scripts * fixed MaraudeManager.all_of() for common Maraudeurs, added color hints in planning * re-instated statistiques module link and first test page * added FinalizeView to send a mail before finalizing compte-rendu * Added PieChart view for FicheStatistique fields * small style updates, added 'age' and 'genre' fields from sujets in statistiques.PieChartView * worked on statistiques, fixed small issues in 'notes' list views * small theme change * removed some dead code * fixed notes.tests, fixed statistiques.info_completed display, added filter in SujetLisView * added some tests * added customised admin templates * added authenticate in CustomAuthenticatationBackend, more verbose login thanks to messages * added django-nose for test coverage * Corrected raising exception on first migration On first migration, qs.exists() would previously be called and raising an Exception, sot he migrations would fail. * Better try block * cleaned up custom settings.py, added some overrides of django base_settings * corrected bad dictionnary key
205 lines
7.2 KiB
Python
205 lines
7.2 KiB
Python
import datetime
|
|
|
|
from django.shortcuts import render, redirect
|
|
from django.contrib import messages
|
|
from django.views import generic
|
|
from django.db.models import (Field, 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 maraudes.notes import Observation
|
|
from maraudes.models import Maraude
|
|
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"
|
|
}
|
|
|
|
|
|
class FilterMixin(generic.edit.FormMixin):
|
|
|
|
form_class = SelectRangeForm
|
|
|
|
def get_initial(self):
|
|
return {'month': self.request.GET.get('month', 0), 'year': self.request.GET.get('year', 0) }
|
|
|
|
def get(self, *args, **kwargs):
|
|
self.year = int(self.request.GET.get('year', 0))
|
|
self.month = int(self.request.GET.get('month', 0))
|
|
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 }
|
|
|
|
def get_observations_queryset(self):
|
|
return Observation.objects.filter(**self._filters('created_date'))
|
|
|
|
def get_maraudes_queryset(self):
|
|
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'))
|
|
|
|
def get_sujets_queryset(self):
|
|
return Sujet.objects.filter(pk__in=self.get_observations_queryset().values_list('sujet'))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['year'] = self.year
|
|
context['month'] = self.month
|
|
return context
|
|
|
|
|
|
NO_DATA = "Aucune donnée"
|
|
|
|
class DashboardView(FilterMixin, generic.TemplateView):
|
|
template_name = "statistiques/index.html"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
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=datetime.time(16,00)
|
|
).count() or NO_DATA
|
|
context['nbr_rencontres'] = rencontres.count() or NO_DATA
|
|
try:
|
|
context['moy_rencontres'] = int(context['nbr_rencontres'] / context['nbr_maraudes'])
|
|
except (ZeroDivisionError, TypeError):
|
|
context['moy_rencontres'] = NO_DATA
|
|
|
|
if self.year and not self.month: #Show rencontres_par_mois graph
|
|
par_mois = rencontres.order_by().annotate(
|
|
mois=ExtractMonth('created_date')
|
|
).values(
|
|
'mois'
|
|
).annotate(
|
|
nbr=Count('pk')
|
|
)
|
|
context['rencontres_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 = rencontres.values('sujet').annotate(nbr=Count('pk')).order_by()
|
|
context['nbr_sujets_rencontres'] = nbr_rencontres.count()
|
|
|
|
|
|
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()
|
|
context['graph_rencontres'] = PieWrapper(
|
|
data= [('Type de rencontre', 'Nombre de sujets')] +
|
|
[(label, get_count_for_range(rg)) for label, rg in categories],
|
|
title= 'Fréquence de rencontres'
|
|
)
|
|
return context
|
|
|
|
|
|
|
|
class PieChartView(FilterMixin, generic.TemplateView):
|
|
template_name = "statistiques/typologie.html"
|
|
|
|
def get_graphs(self):
|
|
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
|
|
queryset = self.get_fichestatistiques_queryset()
|
|
for field in FicheStatistique._meta.fields:
|
|
if field.__class__ in (NullBooleanField, CharField):
|
|
yield str(field.verbose_name), PieWrapper(queryset, field)
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['graphs'] = [(title, graph) for title, graph in self.get_graphs()]
|
|
context['queryset'] = self.get_fichestatistiques_queryset()
|
|
return context
|
|
|
|
|
|
|
|
# AjaxMixin
|
|
|
|
class AjaxOrRedirectMixin:
|
|
""" For view that should be retrieved by Ajax only. If not,
|
|
redirects to the primary view where these are displayed """
|
|
|
|
def get(self, *args, **kwargs):
|
|
""" Redirect to complete details view if request is not ajax """
|
|
if not self.request.is_ajax():
|
|
return redirect("notes:details-sujet", pk=self.get_object().pk)
|
|
return super().get(*args, **kwargs)
|
|
|
|
|
|
|
|
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
|
|
|
model = FicheStatistique
|
|
template_name = "statistiques/fiche_stats_details.html"
|
|
|
|
|
|
|
|
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
|
|
|
model = FicheStatistique
|
|
form_class = StatistiquesForm
|
|
template_name = "statistiques/fiche_stats_update.html"
|