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")