#!/usr/bin/python # -*- mode: python; coding: utf-8 -*- # # stats_cableurs.py # ----------------- # # Copyright (C) 2008, 2009 François Bobot , # Jeremie Dimino , # Michel Blockelet , # Antoine Durand-Gasselin # # 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") # 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") # 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') # 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') # 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)