#! /usr/bin/env python # -*- coding: iso-8859-15 -*- # Copyright (C) Stéphane Glondu # Licence : GPLv2 u"""Ce script permet au trésorier de repérer plus facilement les papiers que les câbleurs n'ont pas encore rendus. Utilisations possibles : * %(prog)s OPTIONS {paiement|carte|list} * %(prog)s OPTIONS mail Les options sont: -d, --debug envoyer tous les mails à l' indiquée, plutôt qu'aux vrais destinataires -h, --help affiche ce message -t, --tri trier suivant ce champ. Les champs possibles sont: id, nom, cableur ou date. Le défaut est ``nom'' -i, --inverse trier par ordre inverse Les commandes sont : * paiement contrôle interactif des nouvelles (ré-)adhésions * carte contrôle interactif des nouvelles cartes d'étudiant * list affiche le récapitulatif des papiers manquants * mail envoyer les mails de rappel aux câbleurs dont le login est dans la , 'bureau' désignant le mail récapitulatif envoyé à bureau ; si la liste est vide tous les mails nécessaires seront envoyés * help affiche ce message Le modèle du mail envoyé aux câbleurs est lu sur l'entrée standard (typiquement via une redirection). Les trois premières lignes doivent être : Encoding: Subject: Le reste est le corps du message. Il doit contenir une occurrence de '%%s' qui sera remplacée par la liste des papiers manquants. Le mail récapitulatif envoyé à bureau est immuable.""" import sys, os, re, getopt sys.path.append('/usr/scripts/gestion') # Fonctions d'affichage from affich_tools import coul, tableau, prompt, cprint # Importation de la base de données from ldap_crans import crans_ldap, ann_scol db = crans_ldap() # Détermination de l'uid du câbleur from user_tests import getuser uid = getuser() if not uid : cprint(u"Impossible de déterminer l'utilisateur !") sys.exit(1) cableur = db.search('uid=%s' % uid)['adherent'][0] # Vérification des droits if u'Tresorier' not in cableur.droits(): cprint(u"Il faut etre tresorier pour exécuter ce script !") sys.exit(1) # Lors des tests, on m'envoie tous les mails ! from socket import gethostname debug = False # Le champ sur lequel on veut trier (id, nom, cableur, date) trier_par = "id" tri_inverse = True if gethostname().split(".")[0] == 'egon': debug = 'glondu@crans.org' ann_scol = 2004 def _controle_interactif_adherents(liste, quoi): """ Contrôle interactif des adhérents de la liste (quoi = p ou c). Retourne (nb_OK, nb_pas_OK). """ if quoi not in 'pc': raise ValueError explicite = {'p': u'du paiement', 'c': u"de la carte d'étudiant"}[quoi] restant = len(liste) if restant == 0: return 0, 0 cprint(u'\nContrôle %s des adhérents' % explicite, 'cyan') cprint(u"Pour chaque entrée, il faut taper 'o' ou 'n' (défaut=n).") cprint(u"Une autre réponse entraîne l'interruption du processus.") cprint(u"Le format est [nb_restant] Nom, Prénom (aid).") cprint(u"") nb = 0 for a in liste: ok = prompt(u'[%3d] %s, %s (%s) ?' % (restant, a.nom(), a.prenom(), a.id()), 'n', '').lower() restant -= 1 if ok == 'o': modifiable = db.search('aid=%s' % a.id(), 'w')['adherent'][0] if modifiable._modifiable: modifiable.controle('+%s' % quoi) cprint(modifiable.save()) nb += 1 else: cprint(u'Adhérent %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge') elif ok != 'n': cprint(u'Arrêt du contrôle %s des adhérents' % explicite, 'rouge') break return nb, len(liste)-nb def _controle_interactif_clubs(liste): """ Contrôle interactif des clubs de la liste (uniquement la charte). Retourne (nb_OK, nb_pas_OK). """ restant = len(liste) if restant == 0: return 0, 0 cprint(u'\nContrôle de la charte des clubs', 'cyan') cprint(u"Pour chaque entrée, il faut taper 'o' ou 'n'.") cprint(u"Une autre réponse entraîne l'interruption du processus.") cprint(u"Le format est [nb_restant] Nom (cid).") cprint(u"") nb = 0 for c in liste: ok = prompt(u'[%2d] %s (%s) ?' % (restant, c.Nom(), c.id()), 'n', '').lower() restant -= 1 if ok == 'o': modifiable = db.search('cid=%s' % c.id(), 'w')['club'][0] if modifiable._modifiable: modifiable.controle('+p') cprint(modifiable.save()) nb += 1 else: cprint(u'Club %s locké, réessayer plus tard' % modifiable.Nom(), 'rouge') elif ok != 'n': cprint(u'Arrêt du contrôle de la charte des clubs', 'rouge') break return nb, len(liste)-nb def qui(historique, quoi): """ Recherche le câbleur qui a effectué la dernière modification quoi dans l'historique, ou qui a inscrit l'adhérent. Retourne le couple (date, cableur) ou ('_inconnu_', '_inconnu_'). """ regexp = re.compile(r'^([^,]*), ([^ :]*)') cableur = ('_inconnu_', '_inconnu_') champ = re.compile(quoi) for ligne in historique: if champ.search(ligne) or 'inscription' in ligne: matched = regexp.search(ligne) if matched: cableur = matched.group(1), matched.group(2) return cableur def controle_interactif(quoi): """ Procédure interactive de contrôle des paiements/chartes (quoi=p) et cartes (quoi=c). """ if quoi not in 'pc': raise ValueError todo_list = db.search('%s=%d&controle!=*%s*' % ({'p': 'paiement', 'c': 'carteEtudiant'}[quoi], ann_scol, quoi)) # Tri de la liste des adhérents selon nom, prénom todo_list['adherent'].sort(key = lambda x: (x.nom(), x.prenom())) if trier_par == 'id': todo_list['adherent'].sort(key = lambda x: int(x.id())) elif trier_par == 'nom': pass else: champ = { 'date': 0, 'cableur': 1 }[trier_par] todo_list['adherent'].sort(key = lambda x: qui(x.historique(), r'paiement')[champ]) # Traitement des adhérents oka, noka = _controle_interactif_adherents(todo_list['adherent'], quoi) if quoi == 'p': # Tri de la liste des clubs todo_list['club'].sort(lambda x, y: cmp(x.nom(), y.nom())) # Traitement des clubs (uniquement la charte) okc, nokc =_controle_interactif_clubs(todo_list['club']) cprint(u'\nRécapitulatif des nouveaux contrôles +%s :' % quoi, 'violet') liste = [[u'adhérents', str(oka), str(noka)]] if quoi == 'p': liste.append([u'clubs', str(okc), str(nokc)]) cprint(tableau(liste, titre = [u'Catégorie', u'OK', u'pas OK'], largeur = [15, 10, 10])) def formater_pour_cableur(liste): """ Formate la liste d'adhérents ou de clubs avec les dates correspondantes. liste est une liste de couples (date, objet). """ lignes = [] total = 0 liste.sort(lambda x, y: cmp(x[1].nom(), y[1].nom())) for date, a in liste: lignes.append([a.id(), a.Nom(), date]) total += 1 if trier_par == 'id': lignes.sort(key = lambda ligne: int(ligne[0])) else: champ = {'nom' : 1, 'cableur' : 1, 'date' : 2}[trier_par] lignes.sort(key = lambda ligne: ligne[champ]) if tri_inverse: lignes.reverse() return tableau(lignes, titre = [u'id', u'Nom', u'Date Heure'], largeur = [6, 40, 18], alignement = ['d', 'c', 'c']) + u'\nTotal : %d' % total def formater_pour_bureau(dico): """ Formate la liste d'adhérents ou de clubs avec les câbleurs correspondantes pour le mail récapitulatif envoyé à bureau. """ lignes = [] total = 0 liste = dico.keys() liste.sort() for cableur in liste: for date, a in dico[cableur]: lignes.append([a.id(), a.Nom(), cableur, date]) total += 1 if trier_par == 'id': lignes.sort(key = lambda ligne: int(ligne[0])) else: champ = {'nom' : 1, 'cableur' : 2, 'date' : 3}[trier_par] lignes.sort(key = lambda ligne: ligne[champ]) if tri_inverse: lignes.reverse() return tableau(lignes, titre = [u'id', u'Nom', u'Câbleur', u'Date'], largeur = [6, 40, 18, 18], alignement = ['d', 'c', 'c', 'c']) + u'\nTotal : %d' % total def chercher_cableurs(liste, quoi): """ Renvoie un dictionnaire cableur->adherents à partir de liste, quoi désigne le champ qui sera recherché dans l'historique. """ resultat = {} for a in liste: date, cableur = qui(a.historique(), quoi) if not resultat.has_key(cableur): resultat[cableur] = [] resultat[cableur].append((date, a)) return resultat from email_tools import send_email, parse_mail_template # Mise en application du mémorable cours de Fred sur les objets :-) class ControleMailer: def __init__(self): # Recherche des câbleurs possédant des cotisations/chartes todo_list = db.search('paiement=%d&controle!=*p*' % ann_scol) self._paiements = chercher_cableurs(todo_list['adherent'], r'(paiement|controle.*\+k)') self._chartes = chercher_cableurs(todo_list['club'], 'paiement') # Recherche des câbleurs possédant des cartes d'étudiant todo_list = db.search('carteEtudiant=%d&controle!=*c*' % ann_scol) self._cartes = chercher_cableurs(todo_list['adherent'], 'carteEtudiant') # Récupère tous les câbleurs qui doivent quelque chose cableurs = {} for a in self._paiements.keys(): if a != '_inconnu_': cableurs[a] = None for a in self._chartes.keys(): if a != '_inconnu_': cableurs[a] = None for a in self._cartes.keys(): if a != '_inconnu_': cableurs[a] = None self._cableurs = cableurs.keys() # Vérification de l'alias pour l'expéditeur if u'tresorier' in cableur.alias(): self._sender = u'%s ' % cableur.Nom() else: self._sender = u'%s <%s@crans.org>' % (cableur.Nom(), cableur.canonical_alias() or cableur.mail()) # Se connecte au serveur SMTP par défaut from smtplib import SMTP self._server = SMTP() self._server.connect() def __del__(self): # Termine la connexion au serveur SMTP self._server.quit() def recapitulatif(self): """ Renvoie le récapitulatif pour le bureau. """ msg = u'' if self._paiements: msg += u"Fiches d'adhésion et cotisations et/ou caution des adhérents :\n" msg += formater_pour_bureau(self._paiements) + '\n\n' if self._chartes: msg += u"Chartes signées des clubs :\n" msg += formater_pour_bureau(self._chartes) + '\n\n' if self._cartes: msg += u"Carte d'étudiant des adhérents :\n" msg += formater_pour_bureau(self._cartes) + '\n\n' return msg.strip() def mail_bureau(self): """ Envoie le mail récapitulatif à bureau (s'il y a qqch à envoyer). """ msg = self.recapitulatif() if msg: msg += u"\n\n-- \nScript exécuté par %s\n" % cableur.Nom() send_email(self._sender, u"Bureau ", u"Récapitulatif des papiers manquants", msg, server = self._server, debug = debug) return coul(u'Mail envoyé à bureau avec succès !', 'vert') else: return coul(u'Tout est à jour, aucun mail envoyé.', 'vert') def mail_cableurs(self, subject, body, liste=None): """ Envoie un mail aux câbleurs concernés qui figurent dans la liste. Si liste vaut None, envoie un mail à tous les câbleurs concernés. Les arguments subject, body permettent de personnaliser (un peu) le mail qui sera envoyé. """ nb = 0 if liste == None: liste = self._cableurs for c in liste: msg = u'' if self._paiements.has_key(c): msg += u"Fiches d'adhésion, cotisations et/ou caution des adhérents :\n" msg += formater_pour_cableur(self._paiements[c]) + '\n\n' if self._chartes.has_key(c): msg += u"Chartes signées des clubs :\n" msg += formater_pour_cableur(self._chartes[c]) + '\n\n' if self._cartes.has_key(c): msg += u"Carte d'étudiant des adhérents :\n" msg += formater_pour_cableur(self._cartes[c]) + '\n\n' if msg: msg = msg.strip() send_email(self._sender, "%s@crans.org" % c, subject, body % msg, server = self._server, debug = debug) nb += 1 return coul(u'%d mail(s) envoyé(s) aux câbleurs avec succès !' % nb, 'vert') def __usage(message=None): """ Comment ça marche ? """ cprint(__doc__ % { 'prog': sys.argv[0] }) if message: cprint(message) sys.exit(1) if __name__ == '__main__' : try: options, arg = getopt.getopt(sys.argv[1:], 'ht:d:i', [ 'help', 'debug=', 'tri=', 'inverse' ]) except getopt.error, msg: __usage(unicode(msg)) for opt, val in options: if opt in [ '-h', '--help' ]: __usage() elif opt in [ '-d', '--debug' ]: debug = val cprint(u'Mode debug, tous les mails seront envoyés à %s.' % debug) elif opt in [ '-t', '--tri' ]: if val in [ 'id', 'nom', 'cableur', 'date' ]: trier_par = val else: __usage(u'Champ de tri invalie %s' % val) elif opt in [ '-i', '--inverse' ]: tri_inverse = True else: __usage("option inconnue « %s »'" % opt) if len(arg) == 0 or arg[0] == 'help': __usage() elif arg[0] == 'paiement': if len(arg) != 1: __usage(u'Mauvaise utilisation de paiement') controle_interactif('p') elif arg[0] == 'carte': if len(arg) != 1: __usage(u'Mauvaise utilisation de carte') controle_interactif('c') elif arg[0] == 'list': if len(arg) != 1: __usage(u'Mauvaise utilisation de list') cprint(ControleMailer().recapitulatif(), newline=False) elif arg[0] == 'mail': mailer = ControleMailer() cableurs = arg[1:] if cableurs: bureau = False if 'bureau' in cableurs: cableurs.remove('bureau') bureau = True else: bureau = True cableurs = mailer._cableurs if cableurs: cprint(u'Des mails vont être envoyés aux câbleurs, lecture du modèle...') subject, body = parse_mail_template(sys.stdin) try: body % u'' except TypeError: cprint(u"Le format du modèle n'est pas correct, arrêt.") sys.exit(1) cprint(u'Modèle OK, on envoie les mails...') cprint(mailer.mail_cableurs(subject, body, cableurs)) if bureau: cprint(mailer.mail_bureau()) else: __usage(u'Commande inconnue : %s' % arg[0]) sys.exit(0) # pydoc n'aime pas l'unicode :-(