diff --git a/app/api/jury.py b/app/api/jury.py index 8005910b6e5e7dcdf383f205048bc11ac234670d..b58b35c2c7e9bfc669b6bcd7216e9882fc9b6c49 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -75,14 +75,12 @@ def decisions_jury(formsemestre_id: int): def _news_delete_jury_etud(etud: Identite, detail: str = ""): "génère news sur effacement décision" # n'utilise pas g.scodoc_dept, pas toujours dispo en mode API - url = url_for( - "scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id - ) ScolarNews.add( typ=ScolarNews.NEWS_JURY, obj=etud.id, - text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""", - url=url, + text=f"""Suppression décision jury {detail} pour {etud.html_link_fiche()}""", + url=etud.url_fiche(), + dept_id=etud.dept_id, ) Scolog.logdb( "jury_delete_manual", diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py index 94ee7091fde077e69489708ed6f16376ab0423a6..72ea51ca254028109a16ed08e440b3acc940c11a 100644 --- a/app/but/jury_but_validation_auto.py +++ b/app/but/jury_but_validation_auto.py @@ -59,6 +59,7 @@ def formsemestre_validation_auto_but( scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ), + dept_id=formsemestre.dept_id, ) return nb_etud_modif, decas diff --git a/app/email.py b/app/email.py index 3f9b03449058a7462209e7b782d1a1c17e9bc166..4f64bd14477ab177d2dbb4be37c51e4470006930 100644 --- a/app/email.py +++ b/app/email.py @@ -67,7 +67,10 @@ def send_message(msg: Message): specified debugging address. """ email_test_mode_address = False - if hasattr(g, "scodoc_dept"): + if current_app.config.get("DISABLE_EMAILS"): + log("send_message: emails disabled by config") + return + if getattr(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( "email_test_mode_address" diff --git a/app/formations/edit_formation.py b/app/formations/edit_formation.py index 593199a09adc826d6acb007f8b48b37019b512ef..b796492a58d420d3f3da76a7c08efab8b52cfb18 100644 --- a/app/formations/edit_formation.py +++ b/app/formations/edit_formation.py @@ -106,13 +106,14 @@ def do_formation_delete(formation_id): formation: Formation = db.session.get(Formation, formation_id) if formation is None: return + dept = formation.departement acronyme = formation.acronyme if formation.formsemestres.count(): raise ScoNonEmptyFormationObject( type_objet="formation", msg=formation.titre, dest_url=url_for( - "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id + "notes.ue_table", scodoc_dept=dept.acronym, formation_id=formation.id ), ) @@ -133,6 +134,7 @@ def do_formation_delete(formation_id): obj=formation_id, text=f"Suppression de la formation {acronyme}", max_frequency=0, + dept_id=dept.id, ) diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py index 6828eb564fcd835549f32765f87fd302f82d8027..aa0e911f052d12079b8901f74b7b3af28791bda5 100644 --- a/app/formations/edit_ue.py +++ b/app/formations/edit_ue.py @@ -111,6 +111,7 @@ def do_ue_create(args, allow_empty_ue_code=False) -> UniteEns: typ=ScolarNews.NEWS_FORM, obj=args["formation_id"], text=f"Modification de la formation {ue.formation.acronyme}", + dept_id=ue.formation.dept_id, ) return ue @@ -195,6 +196,7 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False): typ=ScolarNews.NEWS_FORM, obj=formation.id, text=f"Modification de la formation {formation.acronyme}", + dept_id=formation.dept_id, ) # if not force: diff --git a/app/models/__init__.py b/app/models/__init__.py index 0ca9a70cd38eebfe2892c320b0fcd719d1c3e57d..05315aca97e68d5009afde47425352abdcb09a7c 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -98,19 +98,20 @@ class ScoDocModel(db.Model): # virtual, by default, do nothing return args - def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool: + def from_dict(self, args: dict, excluded: set[str] | None = None) -> list[str]: """Update object's fields given in dict. Add to session but don't commit. - True if modification. + Returns list of modified fields. """ args_dict = self.convert_dict_fields( self.filter_model_attributes(args, excluded=excluded) ) - modified = False + modified = [] for key, value in args_dict.items(): if hasattr(self, key) and value != getattr(self, key): setattr(self, key, value) - modified = True - db.session.add(self) + modified.append(key) + if modified: + db.session.add(self) return modified def to_dict(self) -> dict: @@ -119,9 +120,9 @@ class ScoDocModel(db.Model): d.pop("_sa_instance_state", None) return d - def edit_from_form(self, form) -> bool: + def edit_from_form(self, form) -> list[str]: """Generic edit method for updating model instance. - True if modification. + Returns list of modified fields. """ args = {field.name: field.data for field in form} return self.from_dict(args) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 1dc846e14a76788d9653e0a7f139681d175b3dad..bf337158df12ea3e5787edafabf7bff1d75491f4 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -16,6 +16,7 @@ from sqlalchemy import desc, text from app import db, log from app import models from app.models.departements import Departement +from app.models.events import ScolarNews, Scolog from app.models.scolar_event import ScolarEvent from app.scodoc import notesdb as ndb from app.scodoc.sco_bac import Baccalaureat @@ -294,7 +295,22 @@ class Identite(models.ScoDocModel): """ check_etud_duplicate_code(args, "code_nip", etudid=self.id) check_etud_duplicate_code(args, "code_ine", etudid=self.id) - return super().from_dict(args, **kwargs) + modified = super().from_dict(args, **kwargs) + if modified: + msg = f"""Modification de l'étudiant {self.html_link_fiche()} id={self.id} nip={ + self.code_nip or ''}: {", ".join([f"{k}={args[k]}" for k in modified])}""" + Scolog.logdb( + method="etudident_delete", etudid=self.id, msg=msg, commit=True + ) + # news + ScolarNews.add( + typ=ScolarNews.NEWS_ETUD, + obj=self.id, + text=msg, + max_frequency=0, + dept_id=self.dept_id, + ) + return modified @classmethod def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict: @@ -497,7 +513,11 @@ class Identite(models.ScoDocModel): elif key == "boursier": value = scu.to_bool(value) elif key == "date_naissance": - value = ndb.DateDMYtoISO(value) + value = ( + datetime.date.fromisoformat(ndb.DateDMYtoISO(value)) + if value + else None + ) args_dict[key] = value return args_dict diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 3524579b6b45e3fc564b4ce721ff06b3b385f551..4eb738cf2c140a43812b0174c151f302cb5c4c11 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -149,6 +149,7 @@ class Evaluation(models.ScoDocModel): text=f"""Création d'une évaluation dans <a href="{url}">{ moduleimpl.module.titre_str()}</a>""", url=url, + dept_id=moduleimpl.formsemestre.dept_id, ) return evaluation @@ -217,6 +218,7 @@ class Evaluation(models.ScoDocModel): url }">{modimpl.module.titre}</a>""", url=url, + dept_id=modimpl.formsemestre.dept_id, ) def to_dict(self) -> dict: diff --git a/app/models/events.py b/app/models/events.py index e0285d9c94c75529bda1555dfea068037a487614..c2be93b70b0dddf3ca5577ae6ef2fc788aa49b04 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -67,6 +67,7 @@ class ScolarNews(ScoDocModel): NEWS_ABS = "ABS" # saisie absence NEWS_APO = "APO" # changements de codes APO + NEWS_ETUD = "ETUD" # modification étudiant (object=etudid) NEWS_FORM = "FORM" # modification formation (object=formation_id) NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id) NEWS_JURY = "JURY" # saisie jury @@ -77,6 +78,7 @@ class ScolarNews(ScoDocModel): NEWS_MAP = { NEWS_ABS: "saisie absence", NEWS_APO: "modif. code Apogée", + NEWS_ETUD: "modification étudiant", NEWS_FORM: "modification formation", NEWS_INSCR: "inscription d'étudiants", NEWS_JURY: "saisie jury", @@ -219,7 +221,6 @@ class ScolarNews(ScoDocModel): def notify_by_mail(self): """Notify by email""" formsemestre = self.get_news_formsemestre() - prefs = sco_preferences.SemPreferences( formsemestre_id=formsemestre.id if formsemestre else None ) @@ -232,10 +233,11 @@ class ScolarNews(ScoDocModel): txt = self.text if formsemestre: txt += f"""\n\nSemestre {formsemestre.titre_mois()}\n\n""" - txt += f"""<a href="{url_for("notes.formsemestre_status", _external=True, - scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) - }">{formsemestre.sem_modalite()}</a> - """ + if formsemestre.departement: + txt += f"""<a href="{url_for("notes.formsemestre_status", _external=True, + scodoc_dept=formsemestre.departement.acronym, formsemestre_id=formsemestre.id) + }">{formsemestre.sem_modalite()}</a> + """ user = User.query.filter_by(user_name=self.authenticated_user).first() if user: txt += f"\n\nEffectué par: {user.get_nomcomplet()}\n" @@ -250,10 +252,13 @@ class ScolarNews(ScoDocModel): ) # Transforme les URL en URL absolues - base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] - txt = re.sub('href=/.*?"', 'href="' + base + "/", txt) + if ( + formsemestre and formsemestre.departement + ): # evite pb lors des tests unitaires + base = url_for( + "scolar.index_html", scodoc_dept=formsemestre.departement.acronym + )[: -len("/index_html")] + txt = re.sub('href=/.*?"', 'href="' + base + "/", txt) # Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url' # (si on veut des messages non html) diff --git a/app/models/formations.py b/app/models/formations.py index 9ed7cd0a3e8d2278ae14c767f6ce34456ecfecea..a05573721619acc63951a108cadefabf31e8b35f 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -374,6 +374,7 @@ class Matiere(ScoDocModel): typ=ScolarNews.NEWS_FORM, obj=formation.id, text=f"Modification de la formation {formation.acronyme}", + dept_id=formation.dept_id, ) # cache formation.invalidate_cached_sems() @@ -401,6 +402,7 @@ class Matiere(ScoDocModel): typ=ScolarNews.NEWS_FORM, obj=formation.id, text=f"Modification de la formation {formation.acronyme}", + dept_id=formation.dept_id, ) formation.invalidate_cached_sems() return mat diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index f8c4638d93e88ce8c7f60e59b86c1dda25bff843..acd7b3ee6d6a851d8ce121de47333cb147b823f5 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -270,6 +270,7 @@ class FormSemestre(models.ScoDocModel): text=f"""Création du semestre <a href="{url}">{formsemestre.titre}</a>""", url=url, max_frequency=0, + dept_id=formsemestre.dept_id, ) return formsemestre diff --git a/app/models/modules.py b/app/models/modules.py index 5a8752a55f76948bf0fe73efb05166a74046bcac..37cd3dce10c74cd1bfccdba8c65b702aef773e86 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -153,9 +153,9 @@ class Module(models.ScoDocModel): query = query.filter(Module.id != module_id) return query.count() == 0 - def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool: + def from_dict(self, args: dict, excluded: set[str] | None = None) -> list[str]: """Update object's fields given in dict. Add to session but don't commit. - True if modification. + Returns list of modified fields. - can't change ue nor formation - can change matiere_id, iff new matiere in same ue - can change parcours: parcours list of ApcParcour id or instances. @@ -251,6 +251,7 @@ class Module(models.ScoDocModel): typ=ScolarNews.NEWS_FORM, obj=formation.id, text=f"Modification de la formation {formation.acronyme}", + dept_id=formation.dept_id, ) if inval_cache: formation.invalidate_cached_sems() @@ -301,6 +302,7 @@ class Module(models.ScoDocModel): typ=ScolarNews.NEWS_FORM, obj=formation.id, text=f"Modification de la formation {formation.acronyme}", + dept_id=formation.dept_id, ) formation.invalidate_cached_sems() diff --git a/app/models/ues.py b/app/models/ues.py index 7cf25bc63238a9511339125ec69a7061087ee6c6..dfa3ab05e694987bd85d9121bde14e11785e374a 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -126,9 +126,9 @@ class UniteEns(models.ScoDocModel): return args - def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool: + def from_dict(self, args: dict, excluded: set[str] | None = None) -> list[str]: """Update object's fields given in dict. Add to session but don't commit. - True if modification. + Returns list of modified fields. - can't change formation nor niveau_competence """ return super().from_dict( diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index c926c4ff8b6cb38eeab9d43ddba0dafc925e2cad..d24d64acc149e4355e87ef356305b8a7b2243d3b 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1534,6 +1534,7 @@ def do_formsemestre_delete(formsemestre_id: int): No checks, no warnings: erase all ! """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + dept_id = formsemestre.dept_id sco_cache.EvaluationCache.invalidate_sem(formsemestre.id) titre_sem = formsemestre.titre_annee() # --- Destruction des modules de ce semestre @@ -1689,6 +1690,7 @@ def do_formsemestre_delete(formsemestre_id: int): obj=formsemestre_id, text=f"Suppression du semestre {titre_sem}", max_frequency=0, + dept_id=dept_id, ) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 2cf1794c5c049b6fad7aab480c8ea3de63fec18a..ef289312b9b853e0c07d502fc2e4faa79ba9beb9 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -287,6 +287,7 @@ class BasePreferences: def __init__(self, dept_id: int): dept = db.session.get(Departement, dept_id) if not dept: + breakpoint() log(f"BasePreferences: Invalid departement: {dept_id}") raise ScoValueError(f"BasePreferences: Invalid departement: {dept_id}") self.dept_id = dept.id diff --git a/app/views/scolar.py b/app/views/scolar.py index 756b051e19c64f4f62f744d24ddc0f3f50ee3aa2..8543b1f698ac747977feff521bb1a90d125ba455 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1918,7 +1918,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False): ins.formsemestre_id for ins in etud.formsemestre_inscriptions ] - # delete in all tables ! + # delete in all tables (except scolog) ! # c'est l'ancienne façon de gérer les cascades dans notre pseudo-ORM :) tables = [ "notes_appreciations", @@ -1926,6 +1926,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False): "scolar_formsemestre_validation", "apc_validation_rcue", "apc_validation_annee", + "validation_dut120", "scolar_events", "notes_notes_log", "notes_notes", @@ -1933,7 +1934,6 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False): "notes_formsemestre_inscription", "group_membership", "etud_annotations", - "scolog", "adresse", "absences", "absences_notifications", @@ -1943,6 +1943,19 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False): db.session.execute( sa.text(f"""delete from {table} where etudid=:etudid"""), {"etudid": etudid} ) + nomprenom = etud.nomprenom + nip = etud.code_nip or "" + ine = etud.code_ine or "" + msg = f"Suppression de l'étudiant: {nomprenom} {etudid} (nip {nip}, ine {ine})" + Scolog.logdb(method="etudident_delete", etudid=etudid, msg=msg, commit=True) + # news + ScolarNews.add( + typ=ScolarNews.NEWS_ETUD, + obj=etudid, + text=msg, + max_frequency=0, + dept_id=g.scodoc_dept_id, + ) db.session.delete(etud) db.session.commit() # Inval semestres où il était inscrit: diff --git a/config.py b/config.py index 5781bc1eea6a857a446286e30c82acefa3e2ef5f..5bee4dff48b48b9e14f2e534eac6eea6cee4730b 100755 --- a/config.py +++ b/config.py @@ -80,6 +80,7 @@ class TestConfig(DevConfig): SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "test.gr" DEPT_TEST = "TEST_" # nom du département, ne pas l'utiliser pour un "vrai" SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db1594c208f573ff30e0f6bca" + DISABLE_EMAILS = True # ne pas envoyer de mails class TestAPIConfig(Config): @@ -94,6 +95,7 @@ class TestAPIConfig(Config): ) DEPT_TEST = "TAPI_" # nom du département, ne pas l'utiliser pour un "vrai" SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db15946789Hhahbh88aja175" + DISABLE_EMAILS = True # ne pas envoyer de mails mode = os.environ.get("FLASK_ENV", "production")