#!/bin/bash /usr/scripts/python.sh # -*- coding: utf-8 -*- """alerter les adhérents de leur fin de connexion imminente Ce script devra aussi permettre d'alerter les cableurs sur les prochaines affluences en perm""" import sys import pytz import datetime from dateutil.parser import parse as parse_gtf import calendar from lc_ldap.shortcuts import lc_ldap_readonly from lc_ldap.variables import base_dn import ldap from affich_tools import coul import gestion.mail as mail_module import gestion.config as config PERIODE_TRANSITOIRE = [ datetime.datetime.utcfromtimestamp(date).replace(tzinfo=pytz.UTC) for date in [config.debut_periode_transitoire, config.fin_periode_transitoire] ] #: Une journée (c'est plus pratique) DAY = datetime.timedelta(days=1) #: Format des dates dans la base LDAP FORMAT_LDAP = '%Y%m%d%H%M%S%z' #: Infos à oublier dans un datetime pour ne garder que le jour ERASE_DAY = {'second': 0, 'minute': 0, 'microsecond': 0, 'hour': 0, } #: filtre ldap max(finConnexion) \in intervalle # NB: finConnexion est un attribut ldap multivalué, et on s'intéresse ici # à sa valeur max pour un adhérent. # Les filtres ldap recherchent, de manière existentielle, une valeur valide. # Ainsi, en notant F l'ensemble des valeurs : # max(F) >= v <=> \exists x\in F x>= v <=> finConnexion>=v # L'autre inégalité ( < ) est plus délicate : # max(F) < v <=> \forall x\in F x < v <=> \not( \exists x\in F x >= v ) # <=> \not( finConnexion>= v ) FILTRE_TPL_SIMPLE = u'(&(finConnexion>=%(debut)s)(!(finConnexion>=%(fin)s)))' # Le cas précédent était simplifié, en réalité, la connexion s'achève dès que # l'adhésion se termine. On regarde donc min(max(finConnexion), max(finAdhesion)) # min(a,b) >= v <=> a >= v /\ b >= v # min(a,b) < v <=> a < v \/ b < v FILTRE_TPL = u"""(& (&(finConnexion>=%(debut)s)(finAdhesion>=%(debut)s)) (|(!(finConnexion>=%(fin)s))(!(finAdhesion>=%(fin)s))) )""" # Calcul de la timezone locale #try: # with open('/etc/timezone', 'r') as f: # tz = pytz.timezone(f.read().strip()) #except: # tz = pytz.UTC # usage : # datetime.datetime.now(tz) def warn(mail_conn, adh): """Envoie un mail d'avertissement à ``adh``, en utilisant la connexion mail ``mail_conn``""" tpl_name = 'fin_connexion' fields = ['finAdhesion'] if 'aid' in adh: fields.append('finConnexion') else: tpl_name += '_club' fin = compute_fin_connexion(adh) delai = (fin - datetime.datetime.now(pytz.UTC)).days data = { 'delai': delai, 'adh': adh, } for l in fields: fin = max(parse_gtf(v.value) for v in adh[l]) data[l] = fin deco = min(data[l] for l in fields) if deco >= PERIODE_TRANSITOIRE[0] and deco < PERIODE_TRANSITOIRE[1]: data['sursis'] = PERIODE_TRANSITOIRE[1] data.update({'From': 'respbats@crans.org'}) mail_conn.send_template(tpl_name, data) def compute_fin_connexion(adh): """Renvoie le datetime de fin effective de connexion de l'``adh``""" fields = ['finAdhesion'] if 'aid' in adh: fields.append('finConnexion') value = min(max(parse_gtf(v.value) for v in adh[l]) for l in fields) if value >= PERIODE_TRANSITOIRE[0] and value < PERIODE_TRANSITOIRE[1]: return PERIODE_TRANSITOIRE[1] return value def select(conn, begin, to, mode='r'): """Récupère les adhérents dont la connexion expire entre les datetimes ``begin`` (inclu) et ``to`` (exclu)""" # Nous avons besoin de dates avec timezone. if not begin.tzinfo: begin = begin.replace(tzinfo=pytz.UTC) if not to.tzinfo: to = to.replace(tzinfo=pytz.UTC) # Si la période considérée contient la liste des gens qui seront déco à la # fin de la période transitoire (c'est-à-dire qui seront déco à # PERIODE_TRANSITOIRE[1] ) if begin <= PERIODE_TRANSITOIRE[1] and to > PERIODE_TRANSITOIRE[1]: # Alors il est nécessaire de les considérer, donc de selectionner # à partir du début de la période transitoire, au moins begin = min(PERIODE_TRANSITOIRE[0], begin) # Si la période considérée se termine pendant la période transitoire if to < PERIODE_TRANSITOIRE[1] and to >= PERIODE_TRANSITOIRE[0]: # Alors, il ne faut considérer que les adhérents qui se font déco avant # le début de la période transitoire to = PERIODE_TRANSITOIRE[0] data = { 'debut': begin.strftime(FORMAT_LDAP), 'fin': to.strftime(FORMAT_LDAP), } filtre = FILTRE_TPL % data # NB: on ne prend que les adhérents, d'où SCOPE_ONELEVEL res = conn.search(filtre, scope=ldap.SCOPE_ONELEVEL, dn=base_dn, mode=mode) return res def brief(c, debut, fin): """Renvoie la liste des adhérents dont la connexion expire entre debut et fin""" if not debut.tzinfo: debut = debut.replace(tzinfo=pytz.UTC) if not fin.tzinfo: fin = fin.replace(tzinfo=pytz.UTC) to_warn = select(c, debut, fin) print ("%d adhérents seront prévenus que leur connexion expire entre le %s " + \ "et le %s") % (len(to_warn), debut, fin) if "--list" in sys.argv: for adh in to_warn: valeurs = [max(parse_gtf(v.value) for v in adh[l]) \ for l in ['finConnexion', 'finAdhesion'] ] [f_con, f_adh] = [coul(str(v), 'rouge' if v >= debut and v < fin else 'vert') \ for v in valeurs] print "%r %s %s;%s" % (adh, adh.dn.split(',', 1)[0], f_con, f_adh) return to_warn def prev_calendar(c, date): """Prévisualise l'expiration des connexions sur le mois courant""" month = date.month year = date.year cal = calendar.Calendar() first = datetime.datetime(day=1, month=month, year=year, tzinfo=pytz.UTC) last = first.replace(month=1+month%12, year=year+int(month == 12)) disconnect = select(c, first, last) by_day = {x: 0 for x in xrange(1, 32)} for adh in disconnect: date = compute_fin_connexion(adh) # On veut le .day, mais dans le fuseau UTC (utilisé pour le select juste # avant). Avec UTC, c'est facile: il suffit de virer l'offset de la TZ if date.tzinfo: date -= date.tzinfo.utcoffset(date) by_day[date.day] += 1 yield ['L', 'M', 'Me', 'J', 'V', 'S', 'D'] l = [] for d in cal.itermonthdays(year, month): if not d: item = None else: item = by_day[d] l.append(item) if len(l) == 7: yield l l = [] if l: yield l+(7-len(l))*[None] def ascii_calendar(calendar): """Affiche le calendrier en ascii""" spaces = 3 def pretty_day(x): if x is None: return '' if x == 0: return '.' return str(x) def spaced(x): v = pretty_day(x) return v + (spaces-len(v))*' ' for line in calendar: print ' '.join(map(spaced, line)) def batch_warn(liste): with mail_module.ServerConnection() as mail_conn: for adh in liste: warn(mail_conn, adh) def prev_mail(calendar): """Envoi d'un mail récapitulatif pour ``calendar``""" data = { 'calendar': list(calendar), } From = 'respbats@crans.org' To = From data.update({'To': To, 'From': From}) mailtxt = mail_module.generate('fin_connexion_stats', data) with mail_module.ServerConnection() as mail_conn: mail_conn.sendmail(From, [To], mailtxt.as_string()) if __name__ == '__main__': db = lc_ldap_readonly() now = datetime.datetime.now() args = sys.argv[1:] today = now.replace(**ERASE_DAY) for arg in args: # Applique un delta, si spécifié if arg.startswith('+'): today += int(arg[1:])*DAY print "Nous serons le %s" % today if '--preventif' in args: # * Manuel (préventif): avertit d'une déco dans moins d'un mois liste = select(db, today, today+30*DAY) print "Va envoyer %d mails. Appuyer sur entrée." % len(liste) raw_input() batch_warn(liste) if '--daily' in args: # * Quotidien (préventif) : avertit d'une déco dans moins d'un mois liste = select(db, today+(30-1)*DAY, today+30*DAY) batch_warn(liste) if '--daily-last-chance' in args: # * Quotidien (préventif) : avertit d'une déco dans moins de 4 jours # but: prévenir une dernière fois avant la prochaine perm liste = select(db, today+(4-1)*DAY, today+4*DAY) batch_warn(liste) if '--prev' in args: ascii_calendar(prev_calendar(db, today)) if '--prev-mail' in args: prev_mail(prev_calendar(db, today))