ScoDoc/app/scodoc/sco_abs_notification.py

326 lines
11 KiB
Python
Raw Permalink 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
2023-10-09 23:34:03 +02:00
from flask import current_app, 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.events import Scolog
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
from app.scodoc import sco_etud
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
2023-06-30 15:34:50 +02:00
nbabs, nbabsjust = 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)
# TODO Mettre la fréquence dans les préférences assiduités
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"]:
2021-08-22 13:24:36 +02:00
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
2020-09-26 16:19:37 +02:00
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
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
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
2020-09-26 16:19:37 +02:00
# Variables accessibles dans les balises du template: %(nom_variable)s :
2022-03-05 12:47:08 +01:00
values = sco_bulletins.make_context_dict(formsemestre, etud)
2020-09-26 16:19:37 +02:00
values["nbabs"] = nbabs
values["nbabsjust"] = nbabsjust
values["nbabsnonjust"] = nbabs - nbabsjust
values["url_ficheetud"] = url_for(
2022-03-16 19:06:20 +01:00
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
)
2020-09-26 16:19:37 +02:00
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 = 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})