ScoDoc/app/scodoc/sco_abs_notification.py

344 lines
12 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# 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.
"""
2021-02-04 20:02:44 +01:00
import datetime
from typing import Optional
2020-09-26 16:19:37 +02:00
2024-02-23 19:03:02 +01:00
from flask import flash, g, url_for
2021-08-26 23:43:54 +02:00
from flask_mail import Message
from app import db
from app import email
2021-08-29 19:57:32 +02:00
from app import log
from app.auth.models import User
from app.models.absences import AbsenceNotification
from app.models.etudiants import Identite
from app.models.events import Scolog
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
2020-09-26 16:19:37 +02:00
def abs_notify(etudid: int, date: str | datetime.datetime):
2020-09-26 16:19:37 +02:00
"""Check if notifications are requested and send them
Considère le nombre d'absence dans le semestre courant
2020-09-26 16:19:37 +02:00
(s'il n'y a pas de semestre courant, ne fait rien,
car l'etudiant n'est pas inscrit au moment de l'absence!).
2023-10-09 23:34:03 +02:00
NE FAIT RIEN EN MODE DEBUG.
2020-09-26 16:19:37 +02:00
"""
2023-06-30 15:34:50 +02:00
from app.scodoc import sco_assiduites
# if current_app and current_app.config["DEBUG"]:
# return
2023-10-09 23:34:03 +02:00
2022-03-05 12:47:08 +01:00
formsemestre = retreive_current_formsemestre(etudid, date)
if not formsemestre:
2020-09-26 16:19:37 +02:00
return # non inscrit a la date, pas de notification
2024-03-01 12:40:05 +01:00
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
etudid,
metrique=scu.translate_assiduites_metric(
sco_preferences.get_preference(
"assi_metrique", formsemestre.formsemestre_id
)
),
date_debut=datetime.datetime.combine(
formsemestre.date_debut, datetime.datetime.min.time()
),
date_fin=datetime.datetime.combine(
formsemestre.date_fin, datetime.datetime.min.time()
),
2022-03-05 12:47:08 +01:00
)
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
2020-09-26 16:19:37 +02:00
def do_abs_notify(
formsemestre: FormSemestre,
etudid: int,
date: str | datetime.datetime,
nbabs: int,
nbabsjust: int,
):
"""Given new counts of absences, check if notifications are requested and send them."""
2020-09-26 16:19:37 +02:00
# prefs fallback to global pref if sem is None:
2022-03-05 12:47:08 +01:00
if formsemestre:
formsemestre_id = formsemestre.id
2020-09-26 16:19:37 +02:00
else:
formsemestre_id = None
2022-03-05 12:47:08 +01:00
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
2020-09-26 16:19:37 +02:00
destinations = abs_notify_get_destinations(
2022-03-05 12:47:08 +01:00
formsemestre, prefs, etudid, date, nbabs, nbabsjust
2020-09-26 16:19:37 +02:00
)
2022-03-05 12:47:08 +01:00
msg = abs_notification_message(formsemestre, prefs, etudid, nbabs, nbabsjust)
2020-09-26 16:19:37 +02:00
if not msg:
return # abort
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
2020-09-26 16:19:37 +02:00
destinations_filtered = []
for email_addr in destinations:
nbdays_since_last_notif = user_nbdays_since_last_notif(email_addr, etudid)
2020-09-26 16:19:37 +02:00
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(
destinations_filtered,
etudid,
msg,
nbabs,
nbabsjust,
formsemestre_id,
)
def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id):
2020-09-26 16:19:37 +02:00
"""Actually send the notification by email, and register it in database"""
log(f"abs_notify: sending notification to {destinations}")
2021-08-26 23:43:54 +02:00
for dest_addr in destinations:
msg.recipients = [dest_addr]
email.send_message(msg)
notification = AbsenceNotification(
etudid=etudid,
email=dest_addr,
nbabs=nbabs,
nbabsjust=nbabsjust,
formsemestre_id=formsemestre_id,
2020-09-26 16:19:37 +02:00
)
db.session.add(notification)
2020-09-26 16:19:37 +02:00
Scolog.logdb(
2020-09-26 16:19:37 +02:00
method="abs_notify",
etudid=etudid,
msg=f"sent to {destinations} (nbabs={nbabs})",
commit=True,
2020-09-26 16:19:37 +02:00
)
2022-03-05 12:47:08 +01:00
def abs_notify_get_destinations(
formsemestre: FormSemestre,
prefs: dict,
etudid: int,
date: str | datetime.datetime,
nbabs: int,
nbabsjust: int,
) -> set[str]:
"""Returns set of destination emails to be notified"""
2020-09-26 16:19:37 +02:00
destinations = [] # list of email address to notify
2022-03-05 12:47:08 +01:00
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id):
if prefs["abs_notify_respsem"]:
2020-09-26 16:19:37 +02:00
# notifie chaque responsable du semestre
2022-03-05 12:47:08 +01:00
for responsable in formsemestre.responsables:
if responsable.email:
destinations.append(responsable.email)
2020-09-26 16:19:37 +02:00
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 = Identite.get_etud(etudid)
adresse = etud.adresses.first()
if adresse:
# Mail à utiliser pour les envois vers l'étudiant:
# choix qui pourrait être controlé par une preference
# ici priorité au mail institutionnel:
email_default = adresse.email or adresse.emailperso
if email_default:
destinations.append(email_default)
2020-09-26 16:19:37 +02:00
# 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
2022-03-05 12:47:08 +01:00
if prefs["abs_notify_respeval"]:
mods = mod_with_evals_at_date(date, etudid)
2020-09-26 16:19:37 +02:00
for mod in mods:
u: User = db.session.get(User, mod["responsable_id"])
if u is not None and u.is_active and u.email:
destinations.append(u.email)
2020-09-26 16:19:37 +02:00
# uniq
destinations = set(destinations)
return destinations
def abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
2020-09-26 16:19:37 +02:00
"""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)
2020-09-26 16:19:37 +02:00
(nbabs - nbabs_last_notified) > abs_notify_abs_increment
TODO Mettre à jour avec le module assiduité + fonctionnement métrique
2020-09-26 16:19:37 +02:00
"""
2021-06-14 00:23:22 +02:00
abs_notify_abs_threshold = sco_preferences.get_preference(
"abs_notify_abs_threshold", formsemestre_id
2020-09-26 16:19:37 +02:00
)
2021-06-14 00:23:22 +02:00
abs_notify_abs_increment = sco_preferences.get_preference(
"abs_notify_abs_increment", formsemestre_id
2020-09-26 16:19:37 +02:00
)
nbabs_last_notified = etud_nbabs_last_notified(etudid, formsemestre_id)
2020-09-26 16:19:37 +02:00
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(etudid: int, formsemestre_id: int = None):
2020-09-26 16:19:37 +02:00
"""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)
"""
notifications = (
AbsenceNotification.query.filter_by(etudid=etudid)
.filter(
(AbsenceNotification.formsemestre_id == formsemestre_id)
| (AbsenceNotification.formsemestre_id.is_(None))
)
.order_by(AbsenceNotification.notification_date.desc())
2020-09-26 16:19:37 +02:00
)
last_notif = notifications.first()
return last_notif.nbabs if last_notif else 0
2020-09-26 16:19:37 +02:00
def user_nbdays_since_last_notif(email_addr, etudid) -> Optional[int]:
2020-09-26 16:19:37 +02:00
"""nb days since last notification to this email, or None if no previous notification"""
notifications = AbsenceNotification.query.filter_by(
etudid=etudid, email=email_addr
).order_by(AbsenceNotification.notification_date.desc())
last_notif = notifications.first()
if last_notif:
now = datetime.datetime.now(last_notif.notification_date.tzinfo)
return (now - last_notif.notification_date).days
return None
2020-09-26 16:19:37 +02:00
2022-03-05 12:47:08 +01:00
def abs_notification_message(
formsemestre: FormSemestre, prefs, etudid, nbabs, nbabsjust
):
2020-09-26 16:19:37 +02:00
"""Mime notification message based on template.
2021-08-26 23:43:54 +02:00
returns a Message instance
or None if sending should be canceled (empty template).
2020-09-26 16:19:37 +02:00
"""
from app.scodoc import sco_bulletins
2024-02-03 23:25:05 +01:00
etud = Identite.get_etud(etudid)
2020-09-26 16:19:37 +02:00
# Variables accessibles dans les balises du template: %(nom_variable)s :
2024-02-04 00:07:14 +01:00
values = sco_bulletins.make_context_dict(
formsemestre, etud.to_dict_scodoc7(with_inscriptions=True)
)
2020-09-26 16:19:37 +02:00
values["nbabs"] = nbabs
values["nbabsjust"] = nbabsjust
values["nbabsnonjust"] = nbabs - nbabsjust
values["url_ficheetud"] = url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
)
2020-09-26 16:19:37 +02:00
# Formsemestre concerné (ex: "BUT Informatique semestre 2")
values["semestre"] = formsemestre.titre_num()
2020-09-26 16:19:37 +02:00
template = prefs["abs_notification_mail_tmpl"]
2024-02-23 19:03:02 +01:00
txt = ""
2020-09-26 16:19:37 +02:00
if template:
2024-02-23 19:03:02 +01:00
try:
txt = prefs["abs_notification_mail_tmpl"] % values
except KeyError:
flash("Mail non envoyé: format invalide (voir paramétrage)")
log("abs_notification_message: invalid key in abs_notification_mail_tmpl")
txt = ""
2020-09-26 16:19:37 +02:00
else:
log("abs_notification_message: empty template, not sending message")
2024-02-23 19:03:02 +01:00
if not txt:
2020-09-26 16:19:37 +02:00
return None
2024-02-03 23:25:05 +01:00
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""
msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym))
2021-08-26 23:43:54 +02:00
msg.body = txt
2020-09-26 16:19:37 +02:00
return msg
def retreive_current_formsemestre(
etudid: int, cur_date: str | datetime.date
) -> Optional[FormSemestre]:
2020-09-26 16:19:37 +02:00
"""Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
date est une chaine au format ISO (yyyy-mm-dd) ou un datetime.date
2022-03-05 12:47:08 +01:00
Result: FormSemestre ou None si pas inscrit à la date indiquée
2020-09-26 16:19:37 +02:00
"""
req = """SELECT i.formsemestre_id
FROM notes_formsemestre_inscription i, notes_formsemestre sem
WHERE sem.id = i.formsemestre_id AND i.etudid = %(etudid)s
AND (%(cur_date)s >= sem.date_debut) AND (%(cur_date)s <= sem.date_fin)
"""
2020-09-26 16:19:37 +02:00
r = ndb.SimpleDictFetch(req, {"etudid": etudid, "cur_date": cur_date})
2020-09-26 16:19:37 +02:00
if not r:
return None
# s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
formsemestre = FormSemestre.get_formsemestre(r[0]["formsemestre_id"])
2022-03-05 12:47:08 +01:00
return formsemestre
2020-09-26 16:19:37 +02:00
def mod_with_evals_at_date(
date_abs: str | datetime.datetime, etudid: int
) -> list[dict]:
"""Liste des moduleimpls avec des evaluations à la date indiquée"""
req = """
SELECT m.id AS moduleimpl_id, m.*
FROM notes_moduleimpl m, notes_evaluation e, notes_moduleimpl_inscription i
WHERE m.id = e.moduleimpl_id
AND e.moduleimpl_id = i.moduleimpl_id
AND i.etudid = %(etudid)s
AND e.date_debut <= %(date_abs)s
AND e.date_fin >= %(date_abs)s
"""
return ndb.SimpleDictFetch(req, {"etudid": etudid, "date_abs": date_abs})