#!/bin/bash /usr/scripts/python.sh # -*- coding: utf-8 -*- # Draft de code pour 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 #: 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) c = lc_ldap_readonly() # Instant courant: on ne garde que le jour now = datetime.datetime.now() # Applique un delta, si spécifié for arg in sys.argv[1:]: if arg.startswith('+'): now += int(arg[1:])*DAY today = now.replace(**ERASE_DAY) print "Nous serons le %s" % today def warn(adh, delai): data = {'delai': delai} for l in ['adhesion', 'connexion']: fin = max(parse_gtf(v.value) for v in adh['fin' + l.capitalize()]) fin = fin.date().strftime('%d %B %Y') data['fin_%s' % l] = fin From = 'respbats@crans.org' To = adh.get_mail() if not To: print "No valid mail for %r" % adh return data.update({'To': To, 'From': From}) mailtxt = mail_module.generate('fin_connexion', data) print mailtxt # Test: #warn(c.search(u'aid=4281')[0], 7) #exit() def compute_fin_connexion(adh): return min( max(parse_gtf(v.value) for v in adh['fin' + l]) for l in ['Adhesion', 'Connexion']) 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) 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): 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(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 + 31*DAY # un peu plus probablement disconnect = brief(c, first, last) by_day = {x: 0 for x in xrange(1,32)} for adh in disconnect: date = compute_fin_connexion(adh) by_day[date.day] += 1 spaces = 3 days = ['L', 'M', 'Me', 'J', 'V', 'S', 'D'] print " ".join(d + ' '*(spaces-len(d)) for d in days) l = [] for d in cal.itermonthdays(year, month): if not d: item = "" elif not by_day[d]: item = "." else: item = str(by_day[d]) l.append(item + ' '*(spaces-len(item))) if len(l) == 7: print " ".join(l) l = [] if l: print " ".join(l) # Plusieurs type d'execution: # * Manuel (préventif): avertit d'une déco dans moins d'un mois # select(c, today, today+30*DAY) # * Quotidien (préventif) : avertit d'une déco dans moins d'un mois # select(c, today+(30-1)*DAY, today+30*DAY) # * Quotidien (last chance): avertit d'une déco dans moins de 4 jours # (l'idée, c'est qu'il y a toujours une perm entre les deux) # select(c, today+(4-1)*DAY, today+4*DAY) # * Mensuel: avertit les cableurs des connexions à expiration dans le mois # prochain # select(c, first_day, last_day+DAY) # prev(c, today+28*DAY)