431 lines
16 KiB
Python
431 lines
16 KiB
Python
#!/usr/bin/python
|
|
# -*- mode: python; coding: utf-8 -*-
|
|
#
|
|
# stats_cableurs.py
|
|
# -----------------
|
|
#
|
|
# Copyright (C) 2008, 2009 François Bobot <bobot@crans.org>,
|
|
# Jeremie Dimino <jeremie@dimino.org>,
|
|
# Michel Blockelet <blockelet@crans.org>,
|
|
# Antoine Durand-Gasselin <adg@crans.org>
|
|
#
|
|
# This file is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This file is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
|
|
|
|
import sys, re, datetime
|
|
# hack temporaire pour pallier avoir la version 2.5 de optparse
|
|
#from optparse import OptionParser
|
|
sys.path.append('/usr/scripts/lib')
|
|
from optparse import OptionParser, OptionGroup
|
|
|
|
sys.path.append("/usr/scripts/gestion/")
|
|
from ldap_crans import CransLdap
|
|
from config import ann_scol
|
|
|
|
|
|
|
|
db = CransLdap()
|
|
|
|
__DAYS_IN_PREVIOUS_MONTH = [datetime.timedelta(days) for days in
|
|
[ 31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30 ] ]
|
|
|
|
def parse_ligne_histo(ligne):
|
|
u"""Parse une ligne d'historique et renvoie (['dd', 'mm', 'yyyy'],
|
|
['date', 'heure', 'who', 'what'])"""
|
|
champ = ligne.replace(',', '').replace(': ', '').split(' ')
|
|
sdate = champ[0].split('/')
|
|
date = datetime.date(int(sdate[2]), int(sdate[1]), int(sdate[0]))
|
|
return date, champ
|
|
|
|
def parse_historique(truc):
|
|
u"""Parse l'historique et renvoie la liste des actions sous la forme
|
|
(['dd', 'mm', 'yyyy'], ['date', 'heure', 'who', 'what'])"""
|
|
return [ parse_ligne_histo(ligne) for ligne in truc.historique() ]
|
|
|
|
def dec_date(date):
|
|
u"""retranche un mois à la date."""
|
|
return (date - __DAYS_IN_PREVIOUS_MONTH[date.month -1])
|
|
|
|
|
|
|
|
class StatsCableursBase:
|
|
u"""Classe prototype pour afficher des stats de câbleurs."""
|
|
|
|
colonnes = []
|
|
nbmois = 4
|
|
stats = {}
|
|
score = {}
|
|
kwargs = {}
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
self.kwargs.update(kwargs)
|
|
self.affiche (self.kwargs, 'd')
|
|
|
|
# On regarde s'il y a un mois à partir duquel on veuille partir.
|
|
mois = kwargs.get("mois", -4)
|
|
|
|
# On compte ensuite combien de mois ça fait
|
|
if mois > 0:
|
|
# si on demande depuis 09 (septembre) , on remonte
|
|
# jusqu'à septembre
|
|
self.nbmois = (datetime.date.today().month + 12 - mois) % 12
|
|
else:
|
|
# si on demande les stats sur 4 mois, on remonte sur 4
|
|
# mois
|
|
self.nbmois = - mois
|
|
|
|
# On regarde si on avait pas déjà précisé sur combien de mois on
|
|
# voulait partir.
|
|
self.nbmois = kwargs.get("nbmois", self.nbmois)
|
|
|
|
# bout de code à mon avis explicite
|
|
self.def_columns()
|
|
self.affiche("Génération des stats")
|
|
self.get_stats()
|
|
self.calc_score()
|
|
cableurs = self.sort_cableurs()
|
|
self.print_stats(cableurs)
|
|
|
|
|
|
def def_columns(self):
|
|
u"""Fonction qui sera appelée pour éventuellement mettre à jour
|
|
self.colonnes"""
|
|
pass
|
|
|
|
def get_stats(self):
|
|
u"""Fonction censée récupérer les stats des différents câbleurs,
|
|
et les fouttre dans self.stats"""
|
|
raise NotImplementedError
|
|
|
|
def calc_score(self):
|
|
u"""Fonction censée calculer le score en fonction des stats, et
|
|
le mettre dans self.score"""
|
|
raise NotImplementedError
|
|
|
|
def sort_cableurs(self):
|
|
u"""Renvoie la liste des câbleurs triés, en fonction de leur score"""
|
|
cableurs = self.stats.keys()
|
|
cableurs.sort(lambda x, y : cmp(self.score[x], self.score[y]), reverse=True)
|
|
|
|
if self.kwargs.get('top', 10) != 0:
|
|
cableurs = cableurs[:self.kwargs.get('top', 10)]
|
|
return cableurs
|
|
|
|
def print_stats(self, cableurs):
|
|
u"""Affiche pour chaque cableur les valeurs de ses différentes colonnes
|
|
définies dans self.colonnes."""
|
|
|
|
# On génère la liste des noms sous lesquels on va afficher nos câbleurs
|
|
noms = {}
|
|
for cableur in cableurs:
|
|
try:
|
|
ldap_cableur = db.search('login=%s' % cableur)['adherent'][0]
|
|
ids = { 'nom' : ldap_cableur.nom(),
|
|
'prenom' : ldap_cableur.prenom(),
|
|
'droits' : reduce(unicode.__add__,
|
|
[d[0] for d in ldap_cableur.droits()], u''),
|
|
'tel' : ldap_cableur.tel(),
|
|
'login' : cableur }
|
|
noms[cableur] = self.kwargs.get("fqn", "%(prenom)s %(nom)s") % ids
|
|
except IndexError:
|
|
self.affiche ("Unknown cableur %s!" % cableur, 'd')
|
|
noms[cableur] = cableur+'*'
|
|
|
|
# On récupère la liste des câbleurs, triés en fonction de leur score
|
|
long_max = reduce(lambda m, c : max(m, len(c)), noms.values(), len('cableur'))
|
|
|
|
# Titres des colonnes
|
|
ligne = u"%-*s" % (long_max, u'Câbleur')
|
|
ligne += u" | Score"
|
|
for col in self.colonnes:
|
|
ligne += u" | %s" % col
|
|
print(ligne)
|
|
|
|
# Ligne pour délimiter
|
|
ligne = u'-' * long_max
|
|
ligne += u"-+-%s" % (u'-' * len ("score"))
|
|
for col in self.colonnes:
|
|
ligne += u"-+-%s" % (u'-' * len(col))
|
|
print(ligne)
|
|
|
|
# Statiqtiques par câbleur
|
|
for cableur in cableurs:
|
|
acts = self.stats[cableur]
|
|
ligne = u"%-*s" % (long_max, noms[cableur])
|
|
ligne += u" | %*d" % (len ("score"), self.score[cableur])
|
|
for i in range (len (self.colonnes)):
|
|
ligne += u" | %*d" % (len(self.colonnes[i]), acts[i])
|
|
print(ligne)
|
|
|
|
def sort_events(self, events):
|
|
u"""Split une liste d'évènements selon les mois"""
|
|
sorted_events = []
|
|
since = datetime.date.replace(datetime.date.today(), day = 1)
|
|
backwards = self.nbmois
|
|
|
|
while backwards > 0:
|
|
sorted_events.append([ ev for ev in events if ev[0] >= since ])
|
|
events = [ ev for ev in events if ev[0] < since ]
|
|
backwards -= 1
|
|
since = dec_date(since)
|
|
|
|
sorted_events.append(events)
|
|
return sorted_events
|
|
|
|
def affiche(self, msg, reqs= 'v'):
|
|
u"""Fonction de print qui n'affiche que si on est dans un mode
|
|
kikoolol, debug ou verbose suffisamment avancé."""
|
|
try:
|
|
pool = list (self.kwargs.get('flags', ''))
|
|
for i in reqs:
|
|
pool.remove(i)
|
|
print msg
|
|
except ValueError:
|
|
return
|
|
|
|
|
|
class StatsCableursMachines(StatsCableursBase):
|
|
u"""Cette classe s'occupe de recenser l'activité des différents
|
|
câbleurs sur les machines, ce qui représente le travail typique du
|
|
câbleur en milieu d'année"""
|
|
|
|
kwargs = { 'fqn' : '%(login)s' , 'top' : 0 }
|
|
|
|
def ajoute_actions(self, machine, hist):
|
|
u"""Ajoute dans hist la liste des actions effectués par
|
|
chacuns des câbleurs sur la machine pendant les mois."""
|
|
|
|
for date, champ in parse_historique(machine):
|
|
|
|
# on ne va pas non plus compter les actions qu'on fait sur
|
|
# ses propres machines.
|
|
if machine.proprietaire().compte() != champ[2]:
|
|
|
|
# Il se peut qu'une personne sans les droits câbleur ait
|
|
# effectué une action.
|
|
try:
|
|
hist[champ[2]].append( (date, champ[3]) )
|
|
except KeyError:
|
|
hist[champ[2]] = [ (date, champ[3]) ]
|
|
|
|
def def_columns(self):
|
|
u"""Simplement une par mois, plus une pour toutes les actions
|
|
d'avant."""
|
|
|
|
month = datetime.date.replace(datetime.date.today(), day=1)
|
|
for i in range(self.nbmois):
|
|
self.colonnes.append("%d/%02d" % (month.month, month.year - 2000))
|
|
month = dec_date(month)
|
|
|
|
self.colonnes.append("< " + self.colonnes[-1])
|
|
|
|
|
|
def get_stats(self):
|
|
u"""Récupère les statistiques des différents câbleurs sur les
|
|
différentes machines"""
|
|
|
|
# La liste des câbleurs en tant qu'entité ldap, histoire de bien
|
|
# faire apparaître ceux qui n'ont rien fait
|
|
ldap_cableurs = db.search("droits=cableur")['adherent']
|
|
|
|
# On récupère la liste des machines
|
|
all_machines = db.search('mid=*')
|
|
machines = all_machines['machineFixe'] + all_machines['machineWifi']
|
|
|
|
# hists permet de répartir toutes les actions sur les différents
|
|
# câbleurs.
|
|
hists = {}
|
|
for i in ldap_cableurs:
|
|
hists[i.compte()] = []
|
|
|
|
# on récupère l'historique des machines que l'on met dans hists
|
|
for becane in machines:
|
|
self.ajoute_actions(becane, hists)
|
|
|
|
split_hist = {}
|
|
for cableur in hists.keys():
|
|
split_hist[cableur] = self.sort_events(hists[cableur])
|
|
self.stats[cableur] = [ len(i) for i in split_hist[cableur] ]
|
|
|
|
|
|
def calc_score(self):
|
|
u"""Le calcul se fait simplement comme la somme des actions
|
|
(indépendamment de leur type pour l'instant) sur les mois
|
|
concernés."""
|
|
|
|
for cableur in self.stats.keys():
|
|
self.score[cableur] = sum (self.stats[cableur][:-1])
|
|
|
|
|
|
|
|
class StatsCableursAdherents(StatsCableursBase):
|
|
u"""Cette classe s'occupe de recenser les actions que les câbleurs
|
|
font sur les adhérents (en fait les {,re}adhésions)."""
|
|
|
|
colonnes = [ u'Inscriptions', u'Réinscriptions' ]
|
|
|
|
debut_ann_scol = datetime.date(ann_scol, 8, 1)
|
|
paiement_ann_scol = "paiement+%d" % ann_scol
|
|
|
|
def donne_cableur(self, adherent):
|
|
u""" Cherche le cableur qui a inscrit ou réinscrit un adhérent. """
|
|
|
|
# Est-ce qu'on recherche une inscription ou une reinscription?
|
|
date_inscr = datetime.date.fromtimestamp(adherent.dateInscription())
|
|
if date_inscr < self.debut_ann_scol:
|
|
action = 1 # 'reinscription'
|
|
action_filtre = self.paiement_ann_scol
|
|
else:
|
|
action = 0 # 'inscription'
|
|
action_filtre = 'inscription'
|
|
|
|
for date, champ in parse_historique(adherent):
|
|
if date >= self.debut_ann_scol:
|
|
# Maintenant on regarde si l'action recherchee est ici
|
|
if action_filtre in champ[3:]:
|
|
return action, champ[2]
|
|
|
|
return action, None
|
|
|
|
|
|
def get_stats(self):
|
|
u"""Calcule le nombre d'inscriptions et réinscription pour tous
|
|
les cableurs ayant inscrit ou réinscrit quelqu'un pour l'année
|
|
en cours."""
|
|
|
|
liste = db.search("paiement=%s" % ann_scol)['adherent']
|
|
|
|
for adherent in liste:
|
|
action, cableur = self.donne_cableur(adherent)
|
|
if cableur:
|
|
if not self.stats.has_key(cableur):
|
|
self.stats[cableur] = [0, 0]
|
|
self.stats[cableur][action] += 1
|
|
|
|
|
|
def calc_score(self):
|
|
u"""Calcule le score d'un câbleur, en fonction du nombre
|
|
d'adhésions qu'il a effectué"""
|
|
|
|
# On calcule le score total pour chaque cableur
|
|
for cableur in self.stats.keys():
|
|
self.score[cableur] = 2 * self.stats[cableur][0] + self.stats[cableur][1]
|
|
|
|
|
|
|
|
|
|
def update_fqn(option, opt_str, value, this, fmt):
|
|
u"""Utilisée dans le parser, cette fonction permet de mettre à jour
|
|
correctement la chaîne de format"""
|
|
try:
|
|
if opt_str not in ['-d', '--droits']:
|
|
if re.match ('^ \(%\(droits\)s\)', this.values.fqn):
|
|
this.values.fqn = fmt + this.values.fqn
|
|
else:
|
|
this.values.fqn = this.values.fqn + (' (%s)' % fmt)
|
|
else:
|
|
this.values.fqn = this.values.fqn + fmt
|
|
except TypeError:
|
|
this.values.fqn = fmt
|
|
|
|
def notimplerr(option, opt_str, value, parseur):
|
|
u"""Un callback pour le parser pour renvoyer un NotImplementedError"""
|
|
raise NotImplementedError
|
|
|
|
if __name__ == "__main__":
|
|
usage = "usage: %prog [options]\n %prog [options] [--efficiency -e]"
|
|
parser = OptionParser(usage=usage)
|
|
|
|
# options pour décider le nombre de câbleurs à afficher
|
|
group = OptionGroup(parser, "Options de filtrage",
|
|
"Option pour filtrer les câbleurs à afficher")
|
|
group.add_option('-a', '--all', help= u"Affiche tous les câbleurs",
|
|
action='store_const', const=0, dest='top')
|
|
group.add_option('-t', '--top', metavar= "NB", type= 'int', dest='top',
|
|
help= u"N'affiche que les NB meilleurs câbleurs")
|
|
parser.add_option_group(group)
|
|
|
|
# options pour le format d'affichage des câbleurs
|
|
group = OptionGroup(parser, "Format d'affichage",
|
|
"Pour définir le format d'affichage des câbleurs")
|
|
group.add_option('-d', '--droits', help= u"Affiche les droits du câbleur",
|
|
action='callback', callback= update_fqn,
|
|
callback_kwargs= { 'fmt': ' (%(droits)s)'})
|
|
group.add_option('-F', '--full-name',
|
|
help=u"Affiche Prenom Nom des câbleurs",
|
|
action='callback', callback= update_fqn,
|
|
callback_kwargs= { 'fmt': '%(prenom)s %(nom)s'})
|
|
group.add_option('-l', '--login', help= u"Affiche le login des câbleurs",
|
|
action='callback', callback= update_fqn,
|
|
callback_kwargs= { 'fmt': '%(login)s'})
|
|
parser.add_option('-T', '--tel', help= u"Affiche le n° de téléphone",
|
|
action='callback', callback= update_fqn,
|
|
callback_kwargs= { 'fmt': '%(tel)s'})
|
|
group.add_option('--fqn', dest='fqn',
|
|
help= u"Définit le format d'affichage du nom du câbleur, les champs possibles sont %(droits)s, %(nom)s, %(prenom)s, %(login)s, %(tel)s")
|
|
parser.add_option_group(group)
|
|
|
|
# options de verbosité
|
|
group = OptionGroup(parser, "Commander la verbosité",
|
|
"Pour définir ce que le script doit afficher en plus")
|
|
group.add_option('-D', '--debug',
|
|
help= u"Affiche des informations de debuggage",
|
|
action='store_const', const='d', dest= 'debug')
|
|
group.add_option('-k', '--kikoolol', help="Affiche des trucs kikoolol",
|
|
action='callback', callback= notimplerr)
|
|
group.add_option('-v', '--verbose', help= u"Augmente la verbosité",
|
|
action='store_const', const= 'v', dest= 'verbose')
|
|
parser.add_option_group(group)
|
|
|
|
# options sur la durée étudiée
|
|
group = OptionGroup(parser, "Durée étudiée",
|
|
"Pour définir la durée sur laquelle on travaille")
|
|
group.add_option('-f', '--for', metavar= 'N',
|
|
help= u"Affiche les statistiqes depuis N mois",
|
|
type= 'int', dest='nbmois')
|
|
group.add_option('-s', '--since', metavar= 'MM',
|
|
help= u'Affiche les stats depuis MOIS',
|
|
type= 'int', dest='mois')
|
|
parser.add_option_group(group)
|
|
|
|
# Devrait plutôt être un argument qu'une option
|
|
parser.add_option('-e', '--efficiency',
|
|
help= u"Compte les actions effectuées sur les machines",
|
|
action= 'store_const',
|
|
const= StatsCableursMachines,
|
|
dest= 'default_stats',
|
|
default= StatsCableursAdherents)
|
|
|
|
# on parse, enfin, on laisse optparse le faire pour nous
|
|
(options, args) = parser.parse_args()
|
|
|
|
# Parce que les clefs ont par défaut la valeur None
|
|
kwargs = {}
|
|
for lbl, field in options.__dict__.items():
|
|
if field is not None and lbl != 'default_stats':
|
|
kwargs[lbl] = field
|
|
|
|
# XXX: l'action append_const dans optparse est ajoutée en Python 2.5
|
|
flaglist = []
|
|
if options.verbose:
|
|
flaglist.append("v")
|
|
if options.debug:
|
|
flaglist.append("D")
|
|
flags = "".join(flaglist)
|
|
|
|
kwargs["flags"] = flags
|
|
|
|
# On apelle les stats que l'on veut calculer
|
|
options.default_stats(**kwargs)
|