226 lines
7.5 KiB
Python
Executable file
226 lines
7.5 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)
|
|
# 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))
|
|
|
|
|