diff --git a/app/__init__.py b/app/__init__.py index f929468e01..dbbe1b081b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -247,6 +247,7 @@ def create_app(config_class=DevConfig): migrate.init_app(app, db) login.init_app(app) mail.init_app(app) + app.extensions["mail"].debug = 0 # disable copy of mails to stderr bootstrap.init_app(app) moment.init_app(app) cache.init_app(app) @@ -545,10 +546,9 @@ def log_call_stack(): # Alarms by email: def send_scodoc_alarm(subject, txt): - from app.scodoc import sco_preferences from app import email - sender = sco_preferences.get_preference("email_from_addr") + sender = email.get_from_addr() email.send_email(subject, sender, ["exception@scodoc.org"], txt) diff --git a/app/auth/email.py b/app/auth/email.py index 9ea8f23e0b..e9d1e42625 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -1,14 +1,14 @@ # -*- coding: UTF-8 -* from flask import render_template, current_app from flask_babel import _ -from app.email import send_email +from app.email import get_from_addr, send_email def send_password_reset_email(user): token = user.get_reset_password_token() send_email( "[ScoDoc] Réinitialisation de votre mot de passe", - sender=current_app.config["SCODOC_MAIL_FROM"], + sender=get_from_addr(), recipients=[user.email], text_body=render_template("email/reset_password.txt", user=user, token=token), html_body=render_template("email/reset_password.j2", user=user, token=token), diff --git a/app/email.py b/app/email.py index 241ef07927..a75b2def3c 100644 --- a/app/email.py +++ b/app/email.py @@ -11,6 +11,8 @@ from flask import current_app, g from flask_mail import Message from app import mail +from app.models.departements import Departement +from app.models.config import ScoDocSiteConfig from app.scodoc import sco_preferences @@ -56,6 +58,7 @@ def send_message(msg: Message): In mail debug mode, addresses are discarded and all mails are sent to the specified debugging address. """ + email_test_mode_address = False if hasattr(g, "scodoc_dept"): # on est dans un département, on peut accéder aux préférences email_test_mode_address = sco_preferences.get_preference( @@ -81,6 +84,35 @@ Adresses d'origine: + msg.body ) + current_app.logger.info( + f"""email sent to{' (mode test)' if email_test_mode_address else ''}: {msg.recipients} + from sender {msg.sender} + """ + ) Thread( target=send_async_email, args=(current_app._get_current_object(), msg) ).start() + + +def get_from_addr(dept_acronym: str = None): + """L'adresse "from" à utiliser pour envoyer un mail + + Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe, + prend le `email_from_addr` des préférences de ce département si ce champ est non vide. + Sinon, utilise le paramètre global `email_from_addr`. + Sinon, la variable de config `SCODOC_MAIL_FROM`. + """ + dept_acronym = dept_acronym or getattr(g, "scodoc_dept", None) + if dept_acronym: + dept = Departement.query.filter_by(acronym=dept_acronym).first() + if dept: + from_addr = ( + sco_preferences.get_preference("email_from_addr", dept_id=dept.id) or "" + ).strip() + if from_addr: + return from_addr + return ( + ScoDocSiteConfig.get("email_from_addr") + or current_app.config["SCODOC_MAIL_FROM"] + or "none" + ) diff --git a/app/entreprises/app_relations_entreprises.py b/app/entreprises/app_relations_entreprises.py index 8d1c9c0c4d..b0eef62fde 100644 --- a/app/entreprises/app_relations_entreprises.py +++ b/app/entreprises/app_relations_entreprises.py @@ -216,7 +216,7 @@ def send_email_notifications_entreprise(subject: str, entreprise: Entreprise): txt = "\n".join(txt) email.send_email( subject, - sco_preferences.get_preference("email_from_addr"), + email.get_from_addr(), [EntreprisePreferences.get_email_notifications], txt, ) diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index 4cc539fb18..f9eff362df 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -31,8 +31,8 @@ Formulaires configuration Exports Apogée (codes) from flask import flash, url_for, redirect, request, render_template from flask_wtf import FlaskForm -from wtforms import BooleanField, SelectField, SubmitField - +from wtforms import BooleanField, SelectField, StringField, SubmitField +from wtforms.validators import Email, Optional import app from app.models import ScoDocSiteConfig import app.scodoc.sco_utils as scu @@ -70,6 +70,12 @@ class ScoDocConfigurationForm(FlaskForm): (i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1) ], ) + email_from_addr = StringField( + label="Adresse source des mails", + description="""adresse email source (from) des mails émis par ScoDoc. + Attention: si ce champ peut aussi être défini dans chaque département.""", + validators=[Optional(), Email()], + ) submit_scodoc = SubmitField("Valider") cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) @@ -87,6 +93,7 @@ def configuration(): "enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(), "month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(), "month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(), + "email_from_addr": ScoDocSiteConfig.get("email_from_addr"), } ) if request.method == "POST" and ( @@ -130,6 +137,8 @@ def configuration(): scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_periode2()-1] }""" ) + if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]): + flash("Adresse email origine enregistrée") return redirect(url_for("scodoc.index")) return render_template( diff --git a/app/models/events.py b/app/models/events.py index 39938978b1..6555948d60 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -233,8 +233,7 @@ class ScolarNews(db.Model): txt = re.sub(r'(.*?)', r"\2: \1", txt) subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?") - sender = prefs["email_from_addr"] - + sender = email.get_from_addr() email.send_email(subject, sender, destinations, txt) @classmethod diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index fe973a3fc0..c5d23845bc 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -32,20 +32,21 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence. """ import datetime +from typing import Optional from flask import g, url_for from flask_mail import Message -from app.models.formsemestre import FormSemestre -import app.scodoc.notesdb as ndb -import app.scodoc.sco_utils as scu +from app import db +from app import email from app import log -from app.scodoc.scolog import logdb +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_formsemestre from app.scodoc import sco_preferences from app.scodoc import sco_users -from app import email def abs_notify(etudid, date): @@ -106,32 +107,24 @@ def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust): def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id): """Actually send the notification by email, and register it in database""" - cnx = ndb.GetDBConnexion() - log("abs_notify: sending notification to %s" % destinations) - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + log(f"abs_notify: sending notification to {destinations}") for dest_addr in destinations: msg.recipients = [dest_addr] email.send_message(msg) - ndb.SimpleQuery( - """INSERT into absences_notifications - (etudid, email, nbabs, nbabsjust, formsemestre_id) - VALUES (%(etudid)s, %(email)s, %(nbabs)s, %(nbabsjust)s, %(formsemestre_id)s) - """, - { - "etudid": etudid, - "email": dest_addr, - "nbabs": nbabs, - "nbabsjust": nbabsjust, - "formsemestre_id": formsemestre_id, - }, - cursor=cursor, + notification = AbsenceNotification( + etudid=etudid, + email=dest_addr, + nbabs=nbabs, + nbabsjust=nbabsjust, + formsemestre_id=formsemestre_id, ) + db.session.add(notification) - logdb( - cnx=cnx, + Scolog.logdb( method="abs_notify", etudid=etudid, - msg="sent to %s (nbabs=%d)" % (destinations, nbabs), + msg=f"sent to {destinations} (nbabs={nbabs})", + commit=True, ) @@ -201,39 +194,32 @@ def abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id): return False -def etud_nbabs_last_notified(etudid, formsemestre_id=None): +def etud_nbabs_last_notified(etudid: int, formsemestre_id: int = 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 = ndb.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(), + 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()) ) - res = cursor.dictfetchone() - if res: - return res["nbabs"] - else: - return 0 + last_notif = notifications.first() + return last_notif.nbabs if last_notif else 0 -def user_nbdays_since_last_notif(email_addr, etudid): +def user_nbdays_since_last_notif(email_addr, etudid) -> Optional[int]: """nb days since last notification to this email, or None if no previous notification""" - cnx = ndb.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 + 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 def abs_notification_message( @@ -264,19 +250,19 @@ def abs_notification_message( log("abs_notification_message: empty template, not sending message") return None - subject = """[ScoDoc] Trop d'absences pour %(nomprenom)s""" % etud - msg = Message(subject, sender=prefs["email_from_addr"]) + subject = f"""[ScoDoc] Trop d'absences pour {etud["nomprenom"]}""" + msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym)) msg.body = txt return msg -def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre: +def retreive_current_formsemestre(etudid: int, cur_date) -> Optional[FormSemestre]: """Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée date est une chaine au format ISO (yyyy-mm-dd) Result: FormSemestre ou None si pas inscrit à la date indiquée """ - req = """SELECT i.formsemestre_id + 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) @@ -292,9 +278,8 @@ def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre: def mod_with_evals_at_date(date_abs, etudid): """Liste des moduleimpls avec des evaluations à la date indiquée""" - req = """SELECT m.id AS moduleimpl_id, m.* + 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.jour = %(date_abs)s""" - r = ndb.SimpleDictFetch(req, {"etudid": etudid, "date_abs": date_abs}) - return r + return ndb.SimpleDictFetch(req, {"etudid": etudid, "date_abs": date_abs}) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 7230bffa13..6cfdb25b2a 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -1080,7 +1080,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): subject = f"""Relevé de notes de {etud["nomprenom"]}""" recipients = [recipient_addr] - sender = sco_preferences.get_preference("email_from_addr", formsemestre_id) + sender = email.get_from_addr() if copy_addr: bcc = copy_addr.strip().split(",") else: diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index a4240d4be7..4c87bc4142 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -438,9 +438,7 @@ def notify_etud_change(email_addr, etud, before, after, subject): log("notify_etud_change: sending notification to %s" % email_addr) log("notify_etud_change: subject: %s" % subject) log(txt) - email.send_email( - subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt - ) + email.send_email(subject, email.get_from_addr(), [email_addr], txt) return txt diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py index 213d44b3e0..2dd5273199 100644 --- a/app/scodoc/sco_import_users.py +++ b/app/scodoc/sco_import_users.py @@ -308,5 +308,4 @@ Pour plus d'informations sur ce logiciel, voir %s subject = "Mot de passe ScoDoc" else: subject = "Votre accès ScoDoc" - sender = sco_preferences.get_preference("email_from_addr") - email.send_email(subject, sender, [user["email"]], txt) + email.send_email(subject, email.get_from_addr(), [user["email"]], txt) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 85ab9ea462..07ac4288b0 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -360,12 +360,31 @@ class BasePreferences(object): }, ), # ------------------ MISC + ( + "email_from_addr", + { + "initvalue": "", + "title": "Adresse mail origine", + "size": 40, + "explanation": """adresse expéditeur pour tous les envois par mails (bulletins, + comptes, etc.). + Si vide, utilise la config globale.""", + "category": "misc", + "only_global": True, + }, + ), ( "use_ue_coefs", { "initvalue": 0, - "title": "Utiliser les coefficients d'UE pour calculer la moyenne générale (hors BUT)", - "explanation": """Calcule les moyennes dans chaque UE, puis pondère ces résultats pour obtenir la moyenne générale. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. Attention: changer ce réglage va modifier toutes les moyennes du semestre !. Aucun effet en BUT.""", + "title": """Utiliser les coefficients d'UE pour calculer la moyenne générale + (hors BUT)""", + "explanation": """Calcule les moyennes dans chaque UE, puis pondère ces + résultats pour obtenir la moyenne générale. + Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules + dans lesquels l'étudiant a des notes. Attention: changer ce réglage va modifier toutes + les moyennes du semestre !. Aucun effet en BUT. + """, "input_type": "boolcheckbox", "category": "misc", "labels": ["non", "oui"], @@ -505,7 +524,7 @@ class BasePreferences(object): { "initvalue": 7, "title": "Fréquence maximale de notification", - "explanation": "en jours (pas plus de X envois de mail pour chaque étudiant/destinataire)", + "explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ", "size": 4, "type": "int", "convert_numbers": True, @@ -1569,17 +1588,6 @@ class BasePreferences(object): "category": "bul_mail", }, ), - ( - "email_from_addr", - { - "initvalue": current_app.config["SCODOC_MAIL_FROM"], - "title": "adresse mail origine", - "size": 40, - "explanation": "adresse expéditeur pour les envois par mails (bulletins)", - "category": "bul_mail", - "only_global": True, - }, - ), ( "bul_intro_mail", { @@ -2073,7 +2081,7 @@ class BasePreferences(object): page_title="Préférences", javascripts=["js/detail_summary_persistence.js"], ), - f"

Préférences globales pour {scu.ScoURL()}

", + f"

Préférences globales pour le département {g.scodoc_dept}

", # f"""

modification des logos du département (pour documents pdf)

""" # if current_user.is_administrator() diff --git a/app/templates/configuration.j2 b/app/templates/configuration.j2 index 94726c62b9..9113e3c363 100644 --- a/app/templates/configuration.j2 +++ b/app/templates/configuration.j2 @@ -65,7 +65,7 @@
{{ form_scodoc.hidden_tag() }}
-
+
{{ wtf.quick_form(form_scodoc) }}
diff --git a/app/views/users.py b/app/views/users.py index a9e1af4f57..ef2361be93 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -48,13 +48,13 @@ from wtforms import HiddenField, PasswordField, StringField, SubmitField from wtforms.validators import DataRequired, Email, ValidationError, EqualTo from app import db +from app import email from app.auth.forms import DeactivateUserForm from app.auth.models import Permission from app.auth.models import User from app.auth.models import Role from app.auth.models import UserRole from app.auth.models import is_valid_password -from app.email import send_email from app.models import Departement from app.models.config import ScoDocSiteConfig @@ -212,7 +212,6 @@ def create_user_form(user_name=None, edit=0, all_roles=True): user_name = str(user_name) Role.ensure_standard_roles() # assure la présence des rôles en base auth_dept = current_user.dept - from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email initvalues = {} edit = int(edit) all_roles = int(all_roles) @@ -699,9 +698,10 @@ def create_user_form(user_name=None, edit=0, all_roles=True): token = the_user.get_reset_password_token() else: token = None - send_email( + # Le from doit utiliser la préférence du département de l'utilisateur + email.send_email( "[ScoDoc] Création de votre compte", - sender=from_mail, # current_app.config["ADMINS"][0], + sender=email.get_from_addr(), recipients=[the_user.email], text_body=render_template( "email/welcome.txt", user=the_user, token=token diff --git a/flask_cas/routing.py b/flask_cas/routing.py index 2a66f49450..fbc8809592 100644 --- a/flask_cas/routing.py +++ b/flask_cas/routing.py @@ -32,7 +32,9 @@ def login(): the user's attributes are saved under the key 'CAS_USERNAME_ATTRIBUTE_KEY' """ - + if not "CAS_SERVER" in current_app.config: + current_app.logger.info("cas_login: no configuration") + return "CAS configuration missing" cas_token_session_key = current_app.config["CAS_TOKEN_SESSION_KEY"] redirect_url = create_cas_login_url(