scripts/gestion/mail/fin_connexion.py
2014-11-30 21:21:04 +01:00

222 lines
7.3 KiB
Python
Executable file

#!/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
#: 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"""(&
(aid=*)
(&(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``"""
fin = min(max(parse_gtf(v.value) for v in adh[l]) \
for l in ['finConnexion', 'finAdhesion'] )
delai = (fin - datetime.datetime.now(pytz.UTC)).days
data = {
'delai': delai,
'adh': adh,
}
for l in ['adhesion', 'connexion']:
fin = max(parse_gtf(v.value) for v in adh['fin' + l.capitalize()])
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)
mail_conn.sendmail(From, [To], mailtxt.as_string())
def compute_fin_connexion(adh):
"""Renvoie le datetime de fin effective de connexion de l'``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):
"""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)
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))