Remaster (#38)
* 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
This commit is contained in:
0
statistiques/__init__.py
Normal file
0
statistiques/__init__.py
Normal file
3
statistiques/admin.py
Normal file
3
statistiques/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
8
statistiques/apps.py
Normal file
8
statistiques/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class StatistiquesConfig(AppConfig):
|
||||
name = 'statistiques'
|
||||
|
||||
|
||||
|
||||
|
||||
70
statistiques/charts.py
Normal file
70
statistiques/charts.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from django.db.models import (Field, CharField, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from graphos.sources.simple import SimpleDataSource
|
||||
from graphos.renderers import gchart
|
||||
|
||||
|
||||
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)
|
||||
- a data object and title
|
||||
"""
|
||||
|
||||
height=400
|
||||
width=800
|
||||
labels = {
|
||||
NullBooleanField: {True: "Oui", False: "Non", None:"Ne sait pas"},
|
||||
}
|
||||
|
||||
def __init__( self, queryset=None, field=None,
|
||||
data=None, title=None,
|
||||
null_values=[],**kwargs):
|
||||
if not data:
|
||||
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")
|
||||
|
||||
if field.__class__ in PieWrapper.labels:
|
||||
labels = PieWrapper.labels[field.__class__]
|
||||
elif field.choices:
|
||||
labels = dict(field.choices)
|
||||
else:
|
||||
raise ValueError("Could not guess labels for", field)
|
||||
|
||||
data = ([(field.name, 'count')] + # Headers
|
||||
[(labels[item[field.name]],
|
||||
item['nbr']) for item in queryset.values(
|
||||
field.name
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
).order_by()
|
||||
if (not null_values
|
||||
or item[field] not in null_values)
|
||||
])
|
||||
|
||||
super().__init__(
|
||||
SimpleDataSource(
|
||||
data=data
|
||||
),
|
||||
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),
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
pass
|
||||
42
statistiques/forms.py
Normal file
42
statistiques/forms.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django import forms
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
from .models import FicheStatistique
|
||||
|
||||
class StatistiquesForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FicheStatistique
|
||||
exclude = ["sujet"]
|
||||
|
||||
|
||||
def get_year_range():
|
||||
qs = FicheStatistique.objects.filter(
|
||||
sujet__premiere_rencontre__isnull=False
|
||||
).order_by(
|
||||
'sujet__premiere_rencontre'
|
||||
)
|
||||
year = lambda f: f.sujet.premiere_rencontre.year
|
||||
|
||||
# Need to call exists() in a try block
|
||||
# to avoid raising exception on first migration
|
||||
try:
|
||||
qs_is_not_empty = qs.exists()
|
||||
except OperationalError:
|
||||
qs_is_not_empty = False
|
||||
|
||||
if qs_is_not_empty:
|
||||
return range(year(qs.first()), year(qs.last()) + 1)
|
||||
else:
|
||||
return ()
|
||||
|
||||
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')
|
||||
],
|
||||
)
|
||||
88
statistiques/models.py
Normal file
88
statistiques/models.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.db import models
|
||||
from django.shortcuts import reverse
|
||||
# Create your models here.
|
||||
|
||||
NSP = "Ne sait pas"
|
||||
|
||||
# Item: Parcours institutionnel
|
||||
PARCOURS_INSTITUTIONNEL = "Institutionnel"
|
||||
PARCOURS_FAMILIAL = "Familial"
|
||||
PARCOURS_DE_VIE_CHOICES = (
|
||||
(NSP, "Ne sait pas"),
|
||||
(PARCOURS_FAMILIAL, "Parcours familial"),
|
||||
(PARCOURS_INSTITUTIONNEL, "Parcours institutionnel"),
|
||||
)
|
||||
|
||||
#Item: Type d'habitation
|
||||
HABITATION_SANS = "Sans Abri"
|
||||
HABITATION_LOGEMENT = "Logement"
|
||||
HABITATION_TIERS = "Hébergement"
|
||||
HABITATION_MAL_LOGEMENT = "Mal logé"
|
||||
HABITATION_CHOICES = (
|
||||
(NSP, "Ne sait pas"),
|
||||
(HABITATION_SANS, "Sans abri"),
|
||||
(HABITATION_TIERS, "Hébergé"),
|
||||
(HABITATION_LOGEMENT, "Logé"),
|
||||
(HABITATION_MAL_LOGEMENT, "Mal logé"),
|
||||
)
|
||||
|
||||
#Item: Ressources
|
||||
RESSOURCES_RSA = "RSA"
|
||||
RESSOURCES_AAH = "AAH"
|
||||
RESSOURCES_POLE_EMPLOI = "Pôle Emploi"
|
||||
RESSOURCES_AUTRES = "Autres"
|
||||
RESSOURCES_SANS = "Pas de ressources"
|
||||
RESSOURCES_CHOICES = (
|
||||
(NSP, "Ne sait pas"),
|
||||
(RESSOURCES_AAH, "AAH"),
|
||||
(RESSOURCES_RSA, "RSA"),
|
||||
(RESSOURCES_SANS, "Aucune"),
|
||||
(RESSOURCES_POLE_EMPLOI, "Pôle emploi"),
|
||||
(RESSOURCES_AUTRES, "Autres ressources"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class FicheStatistique(models.Model):
|
||||
|
||||
sujet = models.OneToOneField('notes.Sujet',
|
||||
on_delete=models.CASCADE,
|
||||
primary_key=True,
|
||||
related_name="statistiques")
|
||||
|
||||
# Logement
|
||||
habitation = models.CharField("Type d'habitat", max_length=64,
|
||||
choices=HABITATION_CHOICES,
|
||||
default=NSP)
|
||||
ressources = models.CharField("Ressources", max_length=64,
|
||||
choices=RESSOURCES_CHOICES,
|
||||
default=NSP)
|
||||
connu_siao = models.NullBooleanField("Connu du SIAO ?")
|
||||
|
||||
# Problématiques
|
||||
prob_psychiatrie = models.NullBooleanField("Psychiatrie")
|
||||
prob_administratif = models.NullBooleanField("Administratif")
|
||||
prob_addiction = models.NullBooleanField("Addiction")
|
||||
prob_somatique = models.NullBooleanField("Somatique")
|
||||
|
||||
lien_familial = models.NullBooleanField("Lien Familial")
|
||||
parcours_de_vie = models.CharField("Parcours de vie",
|
||||
max_length=64,
|
||||
choices=PARCOURS_DE_VIE_CHOICES,
|
||||
default=NSP)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('notes:details-sujet', kwargs={'pk': self.sujet.pk})
|
||||
|
||||
@property
|
||||
def info_completed(self):
|
||||
observed = ('prob_psychiatrie', 'prob_addiction',
|
||||
'prob_administratif', 'prob_somatique', 'habitation', 'ressources',
|
||||
'connu_siao', 'lien_familial', 'parcours_de_vie')
|
||||
completed = 0
|
||||
for f in observed:
|
||||
if getattr(self, f) == None or getattr(self, f) == NSP:
|
||||
continue
|
||||
else:
|
||||
completed += 1
|
||||
return int(completed / len(observed) * 100)
|
||||
20
statistiques/templates/statistiques/base.html
Normal file
20
statistiques/templates/statistiques/base.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block extrahead %}
|
||||
<script type="text/javascript" src="{% static "scripts/jquery.flot.min.js" %}"></script>
|
||||
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
|
||||
<script type="text/javascript">
|
||||
google.load("visualization", "1", {packages:["corechart"]});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Statistiques >{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "statistiques:index" %}">Statistiques</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include "statistiques/menu.html" %}
|
||||
{% endblock %}
|
||||
42
statistiques/templates/statistiques/fiche_stats_details.html
Normal file
42
statistiques/templates/statistiques/fiche_stats_details.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% load boolean_icons bootstrap3 %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th colspan="4" class="active">Problématiques</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Psychiatrique</th>
|
||||
<td>{{ object.prob_psychiatrie|as_icon }}</td>
|
||||
<th>Addiction</th>
|
||||
<td>{{ object.prob_addiction|as_icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Administratif</th>
|
||||
<td>{{ object.prob_administratif|as_icon }}</td>
|
||||
<th>Somatique</th>
|
||||
<td>{{ object.prob_somatique|as_icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Habitation</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{{ object.habitation }}</td>
|
||||
<th>Connu du SIAO</th>
|
||||
<td>{{ object.connu_siao|as_icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Ressources</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">{{ object.ressources }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Parcours de vie</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">{{ object.parcours_de_vie }}</td>
|
||||
<th>Lien familial</th>
|
||||
<td>{{ object.lien_familial|as_icon }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
44
statistiques/templates/statistiques/fiche_stats_update.html
Normal file
44
statistiques/templates/statistiques/fiche_stats_update.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% load bootstrap3 %}
|
||||
<form action="{% url "statistiques:update" form.instance.pk %}" method="post" id="update-stats-form">{% csrf_token%}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th colspan="4" class="active">Problématiques</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Psychiatrique</th>
|
||||
<td>{% bootstrap_field form.prob_psychiatrie show_label=False size="small" %}</td>
|
||||
<th>Addiction</th>
|
||||
<td>{% bootstrap_field form.prob_addiction show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Administratif</th>
|
||||
<td>{% bootstrap_field form.prob_administratif show_label=False size="small" %}</td>
|
||||
<th>Somatique</th>
|
||||
<td>{% bootstrap_field form.prob_somatique show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Habitation</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{% bootstrap_field form.habitation show_label=False size="small" %}</td>
|
||||
<th>Connu du SIAO</th>
|
||||
<td>{% bootstrap_field form.connu_siao show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Ressources</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">{% bootstrap_field form.ressources show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Parcours de vie</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">{% bootstrap_field form.parcours_de_vie show_label=False size="small" %}</td>
|
||||
<th>Lien familial</th>
|
||||
<td>{% bootstrap_field form.lien_familial show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" id="submit-form" class="hidden"></input>
|
||||
</form>
|
||||
7
statistiques/templates/statistiques/filter_form.html
Normal file
7
statistiques/templates/statistiques/filter_form.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% load bootstrap3 %}
|
||||
|
||||
<h4>Période</h4>
|
||||
<form action="" method="get">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
{% bootstrap_button "Ok" button_type="submit" %}
|
||||
</form>
|
||||
5
statistiques/templates/statistiques/gchart/html.html
Normal file
5
statistiques/templates/statistiques/gchart/html.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="row text-center" id="wrapper-{{ chart.get_html_id}}">
|
||||
<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>
|
||||
|
||||
13
statistiques/templates/statistiques/gchart/pie_chart.html
Normal file
13
statistiques/templates/statistiques/gchart/pie_chart.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "graphos/gchart/base.html" %}
|
||||
|
||||
{% block create_chart %}
|
||||
var chart_data = data
|
||||
var chart_div = document.getElementById('{{ chart.get_html_id }}');
|
||||
var chart = new google.visualization.PieChart(chart_div);
|
||||
|
||||
// Wait for the chart to finish drawing before calling the getImageURI() method.
|
||||
google.visualization.events.addListener(chart, 'ready', function () {
|
||||
$("#image-{{ chart.get_html_id }}").attr("href", chart.getImageURI());
|
||||
$("#wrapper-{{ chart.get_html_id}}").hide();
|
||||
});
|
||||
{% endblock %}
|
||||
60
statistiques/templates/statistiques/index.html
Normal file
60
statistiques/templates/statistiques/index.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "statistiques/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ block.super }} Maraudes{% endblock %}
|
||||
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li>Maraudes</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class="alert alert-info alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<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 class="col-lg-4">
|
||||
<h3 class="page-header">Données générales</h3>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<span class="badge">{{ nbr_maraudes }}</span>
|
||||
Nombre de maraudes
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="badge">{{ nbr_maraudes_jour }}</span>
|
||||
dont, Maraudes de journée
|
||||
</li>
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<span class="badge">{{ nbr_rencontres }}</span>
|
||||
Nombre total de rencontres
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="badge">{{ moy_rencontres }}</span>
|
||||
soit, en <strong>moyenne</strong> par maraude
|
||||
</li>
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<span class="badge">{{ nbr_sujets_rencontres }}</span>
|
||||
Nombre de sujets rencontrés
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if rencontres_par_mois %}
|
||||
<div class="col-lg-8">
|
||||
<h3 class="page-header">Rencontres par mois</h3>
|
||||
{{ rencontres_par_mois.as_html }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
14
statistiques/templates/statistiques/menu.html
Normal file
14
statistiques/templates/statistiques/menu.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% 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
|
||||
<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 du public
|
||||
<span class="glyphicon glyphicon-adjust"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
46
statistiques/templates/statistiques/typologie.html
Normal file
46
statistiques/templates/statistiques/typologie.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "statistiques/base.html" %}
|
||||
|
||||
{% block title %}{{block.super}} Typologie{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{{block.super}}<li>Typologie</li>{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<hr />
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
<hr />
|
||||
<p>Échantillon : {{ queryset.count }} sujets</p>
|
||||
</div>
|
||||
</div>
|
||||
{% 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();
|
||||
}
|
||||
|
||||
/*$( function() {
|
||||
hideAll();
|
||||
});*/
|
||||
</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>
|
||||
|
||||
{% for title, graph in graphs %}
|
||||
{{ graph.as_html }}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
7
statistiques/tests.py
Normal file
7
statistiques/tests.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
# MANDATORY FEATURES
|
||||
|
||||
# FicheStatistique primary key IS it's foreign sujet pk
|
||||
11
statistiques/urls.py
Normal file
11
statistiques/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url('^$', views.DashboardView.as_view(), name="index"),
|
||||
url('^charts/$', views.PieChartView.as_view(), name="pies"),
|
||||
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"),
|
||||
]
|
||||
|
||||
204
statistiques/views.py
Normal file
204
statistiques/views.py
Normal file
@@ -0,0 +1,204 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user