# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Système de notification par mail des excès d'absences (see ticket #147) Il suffit d'appeler abs_notify() après chaque ajout d'absence. """ import datetime from email.MIMEMultipart import ( # pylint: disable=no-name-in-module,import-error MIMEMultipart, ) from email.MIMEText import MIMEText # pylint: disable=no-name-in-module,import-error from email.MIMEBase import MIMEBase # pylint: disable=no-name-in-module,import-error from email.Header import Header # pylint: disable=no-name-in-module,import-error from email import Encoders # pylint: disable=no-name-in-module,import-error import notesdb as ndb import sco_utils as scu from notes_log import log from scolog import logdb import sco_bulletins import sco_formsemestre def abs_notify(context, etudid, date): """Check if notifications are requested and send them Considère le nombre d'absence dans le semestre courant (s'il n'y a pas de semestre courant, ne fait rien, car l'etudiant n'est pas inscrit au moment de l'absence!). """ sem = retreive_current_formsemestre(context, etudid, date) if not sem: return # non inscrit a la date, pas de notification debut_sem = ndb.DateDMYtoISO(sem["date_debut"]) fin_sem = ndb.DateDMYtoISO(sem["date_fin"]) nbabs = context.CountAbs(etudid, debut=debut_sem, fin=fin_sem) nbabsjust = context.CountAbsJust(etudid, debut=debut_sem, fin=fin_sem) do_abs_notify(context, sem, etudid, date, nbabs, nbabsjust) def do_abs_notify(context, sem, etudid, date, nbabs, nbabsjust): """Given new counts of absences, check if notifications are requested and send them.""" # prefs fallback to global pref if sem is None: if sem: formsemestre_id = sem["formsemestre_id"] else: formsemestre_id = None prefs = context.get_preferences(formsemestre_id=sem["formsemestre_id"]) destinations = abs_notify_get_destinations( context, sem, prefs, etudid, date, nbabs, nbabsjust ) msg = abs_notification_message(context, sem, prefs, etudid, nbabs, nbabsjust) if not msg: return # abort # Vérification fréquence (pour ne pas envoyer de mails trop souvent) abs_notify_max_freq = context.get_preference("abs_notify_max_freq") destinations_filtered = [] for email_addr in destinations: nbdays_since_last_notif = user_nbdays_since_last_notif( context, email_addr, etudid ) if (nbdays_since_last_notif is None) or ( nbdays_since_last_notif >= abs_notify_max_freq ): destinations_filtered.append(email_addr) if destinations_filtered: abs_notify_send( context, destinations_filtered, etudid, msg, nbabs, nbabsjust, formsemestre_id, ) def abs_notify_send( context, destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id ): """Actually send the notification by email, and register it in database""" cnx = context.GetDBConnexion() log("abs_notify: sending notification to %s" % destinations) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) for email in destinations: del msg["To"] msg["To"] = email context.sendEmail(msg) ndb.SimpleQuery( context, """insert into absences_notifications (etudid, email, nbabs, nbabsjust, formsemestre_id) values (%(etudid)s, %(email)s, %(nbabs)s, %(nbabsjust)s, %(formsemestre_id)s)""", vars(), cursor=cursor, ) logdb( cnx=cnx, method="abs_notify", etudid=etudid, msg="sent to %s (nbabs=%d)" % (destinations, nbabs), ) def abs_notify_get_destinations(context, sem, prefs, etudid, date, nbabs, nbabsjust): """Returns set of destination emails to be notified""" formsemestre_id = sem["formsemestre_id"] destinations = [] # list of email address to notify if abs_notify_is_above_threshold( context, etudid, nbabs, nbabsjust, formsemestre_id ): if sem and prefs["abs_notify_respsem"]: # notifie chaque responsable du semestre for responsable_id in sem["responsables"]: u = context.Users.user_info(responsable_id) if u["email"]: destinations.append(u["email"]) if prefs["abs_notify_chief"] and prefs["email_chefdpt"]: destinations.append(prefs["email_chefdpt"]) if prefs["abs_notify_email"]: destinations.append(prefs["abs_notify_email"]) if prefs["abs_notify_etud"]: etud = context.getEtudInfo(etudid=etudid, filled=1)[0] if etud["email_default"]: destinations.append(etud["email_default"]) # Notification (à chaque fois) des resp. de modules ayant des évaluations # à cette date # nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas if sem and prefs["abs_notify_respeval"]: mods = mod_with_evals_at_date(context, date, etudid) for mod in mods: u = context.Users.user_info(mod["responsable_id"]) if u["email"]: destinations.append(u["email"]) # uniq destinations = set(destinations) return destinations def abs_notify_is_above_threshold(context, etudid, nbabs, nbabsjust, formsemestre_id): """True si il faut notifier les absences (indépendemment du destinataire) nbabs: nombre d'absence (de tous types, unité de compte = demi-journée) nbabsjust: nombre d'absences justifiées (nbabs > abs_notify_abs_threshold) (nbabs - nbabs_last_notified) > abs_notify_abs_increment """ abs_notify_abs_threshold = context.get_preference( "abs_notify_abs_threshold", formsemestre_id ) abs_notify_abs_increment = context.get_preference( "abs_notify_abs_increment", formsemestre_id ) nbabs_last_notified = etud_nbabs_last_notified(context, etudid, formsemestre_id) if nbabs_last_notified == 0: if nbabs > abs_notify_abs_threshold: return True # first notification else: return False else: if (nbabs - nbabs_last_notified) >= abs_notify_abs_increment: return True return False def etud_nbabs_last_notified(context, etudid, formsemestre_id=None): """nbabs lors de la dernière notification envoyée pour cet étudiant dans ce semestre ou sans semestre (ce dernier cas est nécessaire pour la transition au nouveau code)""" cnx = context.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """select * from absences_notifications where etudid = %(etudid)s and (formsemestre_id = %(formsemestre_id)s or formsemestre_id is NULL) order by notification_date desc""", vars(), ) res = cursor.dictfetchone() if res: return res["nbabs"] else: return 0 def user_nbdays_since_last_notif(context, email_addr, etudid): """nb days since last notification to this email, or None if no previous notification""" cnx = context.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """select * from absences_notifications where email = %(email_addr)s and etudid=%(etudid)s order by notification_date desc""", {"email_addr": email_addr, "etudid": etudid}, ) res = cursor.dictfetchone() if res: now = datetime.datetime.now(res["notification_date"].tzinfo) return (now - res["notification_date"]).days else: return None def abs_notification_message(context, sem, prefs, etudid, nbabs, nbabsjust): """Mime notification message based on template. returns None if sending should be canceled (emplty template). """ etud = context.getEtudInfo(etudid=etudid, filled=True)[0] # Variables accessibles dans les balises du template: %(nom_variable)s : values = sco_bulletins.make_context_dict(context, sem, etud) values["nbabs"] = nbabs values["nbabsjust"] = nbabsjust values["nbabsnonjust"] = nbabs - nbabsjust values["url_ficheetud"] = context.ScoURL() + "/ficheEtud?etudid=" + etudid template = prefs["abs_notification_mail_tmpl"] if template: txt = prefs["abs_notification_mail_tmpl"] % values else: log("abs_notification_message: empty template, not sending message") return None subject = """Trop d'absences pour %(nomprenom)s""" % etud # msg = MIMEMultipart() subj = Header("[ScoDoc] " + subject, scu.SCO_ENCODING) msg["Subject"] = subj msg["From"] = prefs["email_from_addr"] txt = MIMEText(txt, "plain", scu.SCO_ENCODING) msg.attach(txt) return msg def retreive_current_formsemestre(context, etudid, cur_date): """Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée date est une chaine au format ISO (yyyy-mm-dd) """ req = """SELECT i.formsemestre_id FROM notes_formsemestre_inscription i, notes_formsemestre sem WHERE sem.formsemestre_id = i.formsemestre_id AND i.etudid=%(etudid)s AND (%(cur_date)s >= sem.date_debut) AND (%(cur_date)s <= sem.date_fin)""" r = ndb.SimpleDictFetch(context, req, {"etudid": etudid, "cur_date": cur_date}) if not r: return None # s'il y a plusieurs semestres, prend le premier (rarissime et non significatif): sem = sco_formsemestre.get_formsemestre(context, r[0]["formsemestre_id"]) return sem def mod_with_evals_at_date(context, date_abs, etudid): """Liste des moduleimpls avec des evaluations a la date indiquée""" req = """SELECT m.* FROM notes_moduleimpl m, notes_evaluation e, notes_moduleimpl_inscription i WHERE m.moduleimpl_id = e.moduleimpl_id AND e.moduleimpl_id = i.moduleimpl_id AND i.etudid = %(etudid)s AND e.jour = %(date_abs)s""" r = ndb.SimpleDictFetch(context, req, {"etudid": etudid, "date_abs": date_abs}) return r