diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index d43f597ecebd01972c0f9c633965e47f779a0c70..2298842f8522d3aba3f49c0ff44bbe5b07c50524 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -926,7 +926,7 @@ class ResultatsSemestre(ResultatsCache):
                 group = None  # group (dict) de l'étudiant dans cette partition
                 # dans NotesTableCompat, à revoir
                 etud_etat = self.get_etud_etat(row["etudid"])
-                if etud_etat == "D":
+                if etud_etat == scu.DEMISSION:
                     gr_name = "Dém."
                     row["_tr_class"] = "dem"
                 elif etud_etat == DEF:
diff --git a/app/models/validations.py b/app/models/validations.py
index b24685a87b3ab614d6e3bea029fe88c9b9836b36..fb470b9749f0e0cb87efb9a723312f63b2092d67 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -156,8 +156,16 @@ class ScolarEvent(db.Model):
         db.Integer,
         db.ForeignKey("notes_formsemestre.id"),
     )
+    etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
+    formsemestre = db.relationship(
+        "FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
+    )
 
     def to_dict(self) -> dict:
+        "as a dict"
         d = dict(self.__dict__)
         d.pop("_sa_instance_state", None)
         return d
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py
index 0a7adcaba681782de635ea9dc55b950da947afde..bed3346569fd2f716ee98d53925bb30d328ee558 100644
--- a/app/pe/pe_semestretag.py
+++ b/app/pe/pe_semestretag.py
@@ -41,10 +41,11 @@ from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
 from app.models import FormSemestre
 from app.models.moduleimpls import ModuleImpl
+from app.pe import pe_tagtable
 
 from app.scodoc import sco_codes_parcours
 from app.scodoc import sco_tag_module
-from app.pe import pe_tagtable
+from app.scodoc import sco_utils as scu
 
 
 class SemestreTag(pe_tagtable.TableTag):
@@ -103,7 +104,7 @@ class SemestreTag(pe_tagtable.TableTag):
         self.inscrlist = [
             etud
             for etud in self.nt.inscrlist
-            if self.nt.get_etud_etat(etud["etudid"]) == "I"
+            if self.nt.get_etud_etat(etud["etudid"]) == scu.INSCRIT
         ]
         self.identdict = {
             etudid: ident
diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py
index 56f2b93e30cde70059565091a911b00dce616902..a8322f16642b783fdee4e7ce546be11ff6913a19 100644
--- a/app/scodoc/notes_table.py
+++ b/app/scodoc/notes_table.py
@@ -411,9 +411,9 @@ class NotesTable:
 
     def get_etud_etat_html(self, etudid):
         etat = self.inscrdict[etudid]["etat"]
-        if etat == "I":
+        if etat == scu.INSCRIT:
             return ""
-        elif etat == "D":
+        elif etat == scu.DEMISSION:
             return ' <font color="red">(DEMISSIONNAIRE)</font> '
         elif etat == DEF:
             return ' <font color="red">(DEFAILLANT)</font> '
@@ -465,7 +465,7 @@ class NotesTable:
         vals = []
         for etudid in self.get_etudids():
             # saute les demissionnaires et les défaillants:
-            if self.inscrdict[etudid]["etat"] != "I":
+            if self.inscrdict[etudid]["etat"] != scu.INSCRIT:
                 continue
             val = moys.get(etudid, None)  # None si non inscrit
             try:
@@ -507,8 +507,8 @@ class NotesTable:
         for t in T:
             etudid = t[-1]
             # saute les demissionnaires et les défaillants:
-            if self.inscrdict[etudid]["etat"] != "I":
-                if self.inscrdict[etudid]["etat"] == "D":
+            if self.inscrdict[etudid]["etat"] != scu.INSCRIT:
+                if self.inscrdict[etudid]["etat"] == scu.DEMISSION:
                     nb_dem += 1
                 if self.inscrdict[etudid]["etat"] == DEF:
                     nb_def += 1
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index 227349328e47b00369607af64df1f0e6c8d6ed57..ae2cc60dd87a88efb619f21df6ccb249dda3c632 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -435,11 +435,11 @@ def get_appreciations_list(formsemestre_id: int, etudid: int) -> dict:
 
 def _get_etud_etat_html(etat: str) -> str:
     """chaine html représentant l'état (backward compat sco7)"""
-    if etat == scu.INSCRIT:  # "I"
+    if etat == scu.INSCRIT:
         return ""
-    elif etat == scu.DEMISSION:  # "D"
+    elif etat == scu.DEMISSION:
         return ' <font color="red">(DEMISSIONNAIRE)</font> '
-    elif etat == scu.DEF:  # "DEF"
+    elif etat == scu.DEF:
         return ' <font color="red">(DEFAILLANT)</font> '
     else:
         return f' <font color="red">({etat})</font> '
diff --git a/app/scodoc/sco_cursus_dut.py b/app/scodoc/sco_cursus_dut.py
index f4354c6f1f75c66d77ea728da7419382caab6ab1..32d4796b9259a818bf1c4fe1d55e53d2d261981b 100644
--- a/app/scodoc/sco_cursus_dut.py
+++ b/app/scodoc/sco_cursus_dut.py
@@ -360,7 +360,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
         cur_begin_date = self.sem["dateord"]
         p = []
         for s in self.sems:
-            if s["ins"]["etat"] == "D":
+            if s["ins"]["etat"] == scu.DEMISSION:
                 dem = " (dem.)"
             else:
                 dem = ""
diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py
index 096cc8490021e53ff9e871f0df98ee46e66b16fe..bb869d5a9dbd50aa9ac57cf884180c5876b09913 100644
--- a/app/scodoc/sco_etud.py
+++ b/app/scodoc/sco_etud.py
@@ -1010,7 +1010,7 @@ def descr_situation_etud(etudid: int, ne="") -> str:
         situation = "non inscrit" + ne
     else:
         sem = sco_formsemestre.get_formsemestre(r["formsemestre_id"])
-        if r["etat"] == "I":
+        if r["etat"] == scu.INSCRIT:
             situation = "inscrit%s en %s" % (ne, sem["titremois"])
             # Cherche la date d'inscription dans scolar_events:
             events = scolar_events_list(
diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py
index baec914d876985d1adee73a7e6143eaf3c787dc0..c33b7309fc304b8170084d09464915ed080727ce 100644
--- a/app/scodoc/sco_formsemestre_exterieurs.py
+++ b/app/scodoc/sco_formsemestre_exterieurs.py
@@ -117,7 +117,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
     # Les autres situations (eg redoublements en changeant d'établissement)
     # doivent être gérées par les validations de semestres "antérieurs"
     insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
-        args={"etudid": etudid, "etat": "I"}
+        args={"etudid": etudid, "etat": scu.INSCRIT}
     )
     semlist = [sco_formsemestre.get_formsemestre(i["formsemestre_id"]) for i in insem]
     existing_semestre_ids = {s["semestre_id"] for s in semlist}
diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py
index 8236f2ab7e02f212c43c5fda16226e4e8111cf22..540282e9c02fdde0c6bc05ab3a21ab87c059c6a8 100644
--- a/app/scodoc/sco_formsemestre_inscriptions.py
+++ b/app/scodoc/sco_formsemestre_inscriptions.py
@@ -32,11 +32,13 @@ import time
 import flask
 from flask import flash, url_for, g, request
 
+from app import db
 from app.comp import res_sem
 from app.comp.res_compat import NotesTableCompat
-from app.models import Formation, FormSemestre, FormSemestreInscription
+from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
 from app.models.etudiants import Identite
 from app.models.groups import GroupDescr
+from app.models.validations import ScolarEvent
 import app.scodoc.sco_utils as scu
 from app import log
 from app.scodoc.scolog import logdb
@@ -77,7 +79,7 @@ def do_formsemestre_inscription_listinscrits(formsemestre_id):
     if r is None:
         # retreive list
         r = do_formsemestre_inscription_list(
-            args={"formsemestre_id": formsemestre_id, "etat": "I"}
+            args={"formsemestre_id": formsemestre_id, "etat": scu.INSCRIT}
         )
         sco_cache.SemInscriptionsCache.set(formsemestre_id, r)
     return r
@@ -133,36 +135,51 @@ def do_formsemestre_demission(
     etudid,
     formsemestre_id,
     event_date=None,
-    etat_new="D",  # 'D' or DEF
-    operation_method="demEtudiant",
+    etat_new=scu.DEMISSION,  # DEMISSION or DEF
+    operation_method="dem_etudiant",
     event_type="DEMISSION",
 ):
     "Démission ou défaillance d'un étudiant"
     # marque 'D' ou DEF dans l'inscription au semestre et ajoute
     # un "evenement" scolarite
-    cnx = ndb.GetDBConnexion()
+    if etat_new not in (scu.DEF, scu.DEMISSION):
+        raise ScoValueError("nouveau code d'état invalide")
+    try:
+        event_date_iso = ndb.DateDMYtoISO(event_date)
+    except ValueError as exc:
+        raise ScoValueError("format de date invalide") from exc
+    etud: Identite = Identite.query.filter_by(
+        id=etudid, dept_id=g.scodoc_dept_id
+    ).first_or_404()
     # check lock
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    if not sem["etat"]:
+    formsemestre: FormSemestre = FormSemestre.query.filter_by(
+        id=formsemestre_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    if not formsemestre.etat:
         raise ScoValueError("Modification impossible: semestre verrouille")
     #
-    ins = do_formsemestre_inscription_list(
-        {"etudid": etudid, "formsemestre_id": formsemestre_id}
-    )[0]
-    if not ins:
-        raise ScoException("etudiant non inscrit ?!")
-    ins["etat"] = etat_new
-    do_formsemestre_inscription_edit(args=ins, formsemestre_id=formsemestre_id)
-    logdb(cnx, method=operation_method, etudid=etudid)
-    sco_etud.scolar_events_create(
-        cnx,
-        args={
-            "etudid": etudid,
-            "event_date": event_date,
-            "formsemestre_id": formsemestre_id,
-            "event_type": event_type,
-        },
+    if formsemestre_id not in (inscr.formsemestre_id for inscr in etud.inscriptions()):
+        raise ScoValueError("étudiant non inscrit dans ce semestre !")
+    inscr = next(
+        inscr
+        for inscr in etud.inscriptions()
+        if inscr.formsemestre_id == formsemestre_id
+    )
+    inscr.etat = etat_new
+    db.session.add(inscr)
+    Scolog.logdb(method=operation_method, etudid=etudid)
+    event = ScolarEvent(
+        etudid=etudid,
+        event_date=event_date_iso,
+        formsemestre_id=formsemestre_id,
+        event_type=event_type,
     )
+    db.session.add(event)
+    db.session.commit()
+    if etat_new == scu.DEMISSION:
+        flash("Démission enregistrée")
+    elif etat_new == scu.DEF:
+        flash("Défaillance enregistrée")
 
 
 def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
@@ -250,7 +267,7 @@ def do_formsemestre_inscription_with_modules(
     formsemestre_id,
     etudid,
     group_ids=[],
-    etat="I",
+    etat=scu.INSCRIT,
     etape=None,
     method="inscription_with_modules",
 ):
@@ -467,7 +484,7 @@ def formsemestre_inscription_with_modules(
             formsemestre_id,
             etudid,
             group_ids=group_ids,
-            etat="I",
+            etat=scu.INSCRIT,
             method="formsemestre_inscription_with_modules",
         )
         return flask.redirect(
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index 177b9758194950c5314a6389ca226be961ff1f73..f129a36a21c5d2c0e9b2f6c2fc9ceda0d6bcb209 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -191,11 +191,11 @@ def formsemestre_validation_etud_form(
     )
 
     etud_etat = nt.get_etud_etat(etudid)
-    if etud_etat == "D":
+    if etud_etat == scu.DEMISSION:
         H.append('<div class="ue_warning"><span>Etudiant démissionnaire</span></div>')
-    if etud_etat == DEF:
+    if etud_etat == scu.DEF:
         H.append('<div class="ue_warning"><span>Etudiant défaillant</span></div>')
-    if etud_etat != "I":
+    if etud_etat != scu.INSCRIT:
         H.append(
             tf_error_message(
                 f"""Impossible de statuer sur cet étudiant:
@@ -357,7 +357,7 @@ def formsemestre_validation_etud_form(
 
     H.append(
         f"""<div class="link_defaillance">Ou <a class="stdlink" href="{
-            url_for("scolar.formDef", scodoc_dept=g.scodoc_dept, etudid=etudid, 
+            url_for("scolar.form_def", scodoc_dept=g.scodoc_dept, etudid=etudid, 
                     formsemestre_id=formsemestre_id)
             }">déclarer l'étudiant comme défaillant dans ce semestre</a></div>"""
     )
@@ -910,7 +910,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
             )[0]
 
             # Conditions pour validation automatique:
-            if ins["etat"] == "I" and (
+            if ins["etat"] == scu.INSCRIT and (
                 (
                     (not Se.prev)
                     or (
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index ffee6a7e6d540f590cf30fcf283ee143e45fd2bb..4da76d313066dda8a27d50f6a3af37cb37e0c4b0 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -308,9 +308,9 @@ def get_group_infos(group_id, etat=None):  # was _getlisteetud
     # add human readable description of state:
     nbdem = 0
     for t in members:
-        if t["etat"] == "I":
+        if t["etat"] == scu.INSCRIT:
             t["etath"] = ""  # etudiant inscrit, ne l'indique pas dans la liste HTML
-        elif t["etat"] == "D":
+        elif t["etat"] == scu.DEMISSION:
             events = sco_etud.scolar_events_list(
                 cnx,
                 args={
diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py
index 53b4563d8f2154bf0bb498954ce507cfaab3c205..89338e8bcb5a0a5a0c26f1a2a78d70af59671c1d 100644
--- a/app/scodoc/sco_import_etuds.py
+++ b/app/scodoc/sco_import_etuds.py
@@ -545,7 +545,7 @@ def _import_one_student(
     args["description"] = "(infos admission)"
     _ = sco_etud.adresse_create(cnx, args)
     # Inscription au semestre
-    args["etat"] = "I"  # etat insc. semestre
+    args["etat"] = scu.INSCRIT  # etat insc. semestre
     if formsemestre_id:
         args["formsemestre_id"] = formsemestre_id
     else:
@@ -576,7 +576,7 @@ def _import_one_student(
         int(args["formsemestre_id"]),
         etudid,
         group_ids,
-        etat="I",
+        etat=scu.INSCRIT,
         method="import_csv_file",
     )
     return args["formsemestre_id"]
diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py
index 6ab1baf104602ec7d138a9fa60cfe69c88829a1f..2d3b644f0bcddd74f7ff79504f61c02eb1f2c1ff 100644
--- a/app/scodoc/sco_inscr_passage.py
+++ b/app/scodoc/sco_inscr_passage.py
@@ -175,12 +175,12 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
     (la liste doit avoir été vérifiée au préalable)
     En option: inscrit aux mêmes groupes que dans le semestre origine
     """
-    log("do_inscrit (inscrit_groupes=%s): %s" % (inscrit_groupes, etudids))
+    log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
     for etudid in etudids:
         sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
             sem["formsemestre_id"],
             etudid,
-            etat="I",
+            etat=scu.INSCRIT,
             method="formsemestre_inscr_passage",
         )
         if inscrit_groupes:
diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py
index 54d53e936d66a13a312fc8c07f0b079856649a1c..b3aeba7ade1132bf8c983bec1ea2c0d65d7bb41f 100644
--- a/app/scodoc/sco_liste_notes.py
+++ b/app/scodoc/sco_liste_notes.py
@@ -325,11 +325,11 @@ def _make_table_notes(
         if etud is None:
             continue
 
-        if etat == "I":  # si inscrit, indique groupe
+        if etat == scu.INSCRIT:  # si inscrit, indique groupe
             groups = sco_groups.get_etud_groups(etudid, modimpl_o["formsemestre_id"])
             grc = sco_groups.listgroups_abbrev(groups)
         else:
-            if etat == "D":
+            if etat == scu.DEMISSION:
                 grc = "DEM"  # attention: ce code est re-ecrit plus bas, ne pas le changer (?)
                 css_row_class = "etuddem"
             else:
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index 79a33d0d5dd8c2a3b8000873d169648628bbb428..12828e870b9ec9e29015edda390615c3c7db5f7e 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -75,18 +75,18 @@ def _menuScolarite(authuser, sem, etudid):
 
     if ins["etat"] != "D":
         dem_title = "Démission"
-        dem_url = "scolar.formDem"
+        dem_url = "scolar.form_dem"
     else:
         dem_title = "Annuler la démission"
-        dem_url = "scolar.doCancelDem"
+        dem_url = "scolar.do_cancel_dem"
 
     # Note: seul un etudiant inscrit (I) peut devenir défaillant.
     if ins["etat"] != sco_codes_parcours.DEF:
         def_title = "Déclarer défaillance"
-        def_url = "scolar.formDef"
+        def_url = "scolar.form_def"
     elif ins["etat"] == sco_codes_parcours.DEF:
         def_title = "Annuler la défaillance"
-        def_url = "scolar.doCancelDef"
+        def_url = "scolar.do_cancel_def"
     def_enabled = (
         (ins["etat"] != "D")
         and authuser.has_permission(Permission.ScoEtudInscrit)
@@ -230,7 +230,7 @@ def ficheEtud(etudid=None):
         info["last_formsemestre_id"] = ""
     sem_info = {}
     for sem in info["sems"]:
-        if sem["ins"]["etat"] != "I":
+        if sem["ins"]["etat"] != scu.INSCRIT:
             descr, _ = etud_descr_situation_semestre(
                 etudid,
                 sem["formsemestre_id"],
@@ -554,7 +554,7 @@ def menus_etud(etudid):
         },
         {
             "title": "Changer la photo",
-            "endpoint": "scolar.formChangePhoto",
+            "endpoint": "scolar.form_change_photo",
             "args": {"etudid": etud["etudid"]},
             "enabled": authuser.has_permission(Permission.ScoEtudChangeAdr),
         },
diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py
index 41b29259bf5eed7361d72245e266c5a38481011b..36e9ca0ec8f397c4e038b43a990a9e4f714a6666 100644
--- a/app/scodoc/sco_poursuite_dut.py
+++ b/app/scodoc/sco_poursuite_dut.py
@@ -115,7 +115,7 @@ def etud_get_poursuite_info(sem, etud):
                         code_semestre_validant(dec["code"])
                         or code_semestre_attente(dec["code"])
                     )
-                    and nt.get_etud_etat(etudid) == "I"
+                    and nt.get_etud_etat(etudid) == scu.INSCRIT
                 ):
                     d = [
                         ("moy", scu.fmt_note(nt.get_etud_moy_gen(etudid))),
diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py
index 27427aebca76c3699f39910a79e496880a0f3f6f..3edcf66076e96837d70d58d59381b31a32aed9aa 100644
--- a/app/scodoc/sco_pvjury.py
+++ b/app/scodoc/sco_pvjury.py
@@ -350,7 +350,9 @@ def dict_pvjury(
             if Se.prev and Se.prev_decision:
                 d["prev_decision_sem"] = Se.prev_decision
                 d["prev_code"] = Se.prev_decision["code"]
-                d["prev_code_descr"] = _descr_decision_sem("I", Se.prev_decision)
+                d["prev_code_descr"] = _descr_decision_sem(
+                    scu.INSCRIT, Se.prev_decision
+                )
                 d["prev"] = Se.prev
                 has_prev = True
             else:
diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py
index 6978e2eb3b8712cc52efadad88e926365428e70e..31e9d90ef80f0fcbcec85f7ced215d9ad7af6c70 100644
--- a/app/scodoc/sco_report.py
+++ b/app/scodoc/sco_report.py
@@ -1315,7 +1315,7 @@ def graph_parcours(
                     s["semestre_id"] == nt.parcours.NB_SEM
                     and dec
                     and code_semestre_validant(dec["code"])
-                    and nt.get_etud_etat(etudid) == "I"
+                    and nt.get_etud_etat(etudid) == scu.INSCRIT
                 ):
                     # cas particulier du diplome puis poursuite etude
                     edges[
@@ -1360,7 +1360,7 @@ def graph_parcours(
                 if (
                     dec
                     and code_semestre_validant(dec["code"])
-                    and nt.get_etud_etat(etudid) == "I"
+                    and nt.get_etud_etat(etudid) == scu.INSCRIT
                 ):
                     nid = sem_node_name(s, "_dipl_")
                     edges[(sem_node_name(s), nid)].add(etudid)
diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
index 9c3887a739bdd96caa0f189c40f3ade54ab2477c..bfcd0f81f6909e856f6d1abc212290fd74534f5d 100644
--- a/app/scodoc/sco_synchro_etuds.py
+++ b/app/scodoc/sco_synchro_etuds.py
@@ -658,7 +658,7 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
             sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
                 sem["formsemestre_id"],
                 args["etudid"],
-                etat="I",
+                etat=scu.INSCRIT,
                 etape=args["etape"],
                 method="synchro_apogee",
             )
diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py
index fd11da4e89f55795025cebf2991dca26b426e47a..85867b1e872135c21749e4baf5cb1e2e131bc9fd 100644
--- a/app/scodoc/sco_trombino_tours.py
+++ b/app/scodoc/sco_trombino_tours.py
@@ -145,7 +145,9 @@ def pdf_trombino_tours(
 
     for group_id in groups_infos.group_ids:
         if group_id != "None":
-            members, _, group_tit, sem, _ = sco_groups.get_group_infos(group_id, "I")
+            members, _, group_tit, sem, _ = sco_groups.get_group_infos(
+                group_id, scu.INSCRIT
+            )
             groups += " %s" % group_tit
             L = []
             currow = []
@@ -391,7 +393,7 @@ def pdf_feuille_releve_absences(
     )
 
     for group_id in groups_infos.group_ids:
-        members, _, group_tit, _, _ = sco_groups.get_group_infos(group_id, "I")
+        members, _, group_tit, _, _ = sco_groups.get_group_infos(group_id, scu.INSCRIT)
         L = []
 
         currow = [
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 296f22c9460cf9841e4f359f7915b71066525dd9..ce001b544245403080f8e41a96ebab62781ec853 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -1034,6 +1034,10 @@ div.sco_help {
   background-color: rgb(200, 200, 220);
 }
 
+div.vertical_spacing_but {
+  margin-top: 12px;
+}
+
 span.wtf-field ul.errors li {
   color: red;
 }
diff --git a/app/views/scolar.py b/app/views/scolar.py
index ed8e93d726ce9f40e6f2647659f1271578a14cff..505cff5be0a9d587caddcf0a5e0979fa3c59b4c1 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -52,12 +52,11 @@ from app.decorators import (
     admin_required,
     login_required,
 )
-from app.models import formsemestre
 from app.models.etudiants import Identite
 from app.models.etudiants import make_etud_args
-from app.models.events import ScolarNews
+from app.models.events import ScolarNews, Scolog
 from app.models.formsemestre import FormSemestre
-
+from app.models.validations import ScolarEvent
 from app.views import scolar_bp as bp
 from app.views import ScoData
 
@@ -972,11 +971,11 @@ def etud_photo_orig_page(etudid=None):
     return "\n".join(H)
 
 
-@bp.route("/formChangePhoto", methods=["GET", "POST"])
+@bp.route("/form_change_photo", methods=["GET", "POST"])
 @scodoc
 @permission_required(Permission.ScoEtudChangeAdr)
 @scodoc7func
-def formChangePhoto(etudid=None):
+def form_change_photo(etudid=None):
     """Formulaire changement photo étudiant"""
     etud = sco_etud.get_etud_info(filled=True)[0]
     if sco_photos.etud_photo_is_local(etud):
@@ -1014,7 +1013,7 @@ def formChangePhoto(etudid=None):
         return (
             "\n".join(H)
             + tf[1]
-            + '<p><a class="stdlink" href="formSuppressPhoto?etudid=%s">Supprimer cette photo</a></p>'
+            + '<p><a class="stdlink" href="form_suppress_photo?etudid=%s">Supprimer cette photo</a></p>'
             % etudid
             + html_sco_header.sco_footer()
         )
@@ -1032,13 +1031,15 @@ def formChangePhoto(etudid=None):
     return "\n".join(H) + html_sco_header.sco_footer()
 
 
-@bp.route("/formSuppressPhoto", methods=["POST", "GET"])
+@bp.route("/form_suppress_photo", methods=["POST", "GET"])
 @scodoc
 @permission_required(Permission.ScoEtudChangeAdr)
 @scodoc7func
-def formSuppressPhoto(etudid=None, dialog_confirmed=False):
+def form_suppress_photo(etudid=None, dialog_confirmed=False):
     """Formulaire suppression photo étudiant"""
-    etud = Identite.query.get_or_404(etudid)
+    etud: Identite = Identite.query.filter_by(
+        id=etudid, dept_id=g.scodoc_dept_id
+    ).first_or_404()
     if not dialog_confirmed:
         return scu.confirm_dialog(
             f"<p>Confirmer la suppression de la photo de {etud.nom_disp()} ?</p>",
@@ -1057,99 +1058,91 @@ def formSuppressPhoto(etudid=None, dialog_confirmed=False):
 
 
 #
-@bp.route("/formDem")
+@bp.route("/form_dem")
 @scodoc
 @permission_required(Permission.ScoEtudInscrit)
 @scodoc7func
-def formDem(etudid, formsemestre_id):
+def form_dem(etudid, formsemestre_id):
     "Formulaire Démission Etudiant"
-    return _formDem_of_Def(
+    return _form_dem_of_def(
         etudid,
         formsemestre_id,
         operation_name="Démission",
-        operation_method="doDemEtudiant",
+        operation_method="do_dem_etudiant",
     )
 
 
-@bp.route("/formDef")
+@bp.route("/form_def")
 @scodoc
 @permission_required(Permission.ScoEtudInscrit)
 @scodoc7func
-def formDef(etudid, formsemestre_id):
+def form_def(etudid, formsemestre_id):
     "Formulaire Défaillance Etudiant"
-    return _formDem_of_Def(
+    return _form_dem_of_def(
         etudid,
         formsemestre_id,
         operation_name="Défaillance",
-        operation_method="doDefEtudiant",
+        operation_method="do_def_etudiant",
     )
 
 
-def _formDem_of_Def(
-    etudid,
-    formsemestre_id,
-    operation_name="",
-    operation_method="",
+def _form_dem_of_def(
+    etudid: int,
+    formsemestre_id: int,
+    operation_name: str = "",
+    operation_method: str = "",
 ):
     "Formulaire démission ou défaillance Etudiant"
-    etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    if not sem["etat"]:
+    etud: Identite = Identite.query.filter_by(
+        id=etudid, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    formsemestre: FormSemestre = FormSemestre.query.filter_by(
+        id=formsemestre_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    if not formsemestre.etat:
         raise ScoValueError("Modification impossible: semestre verrouille")
-
-    etud["formsemestre_id"] = formsemestre_id
-    etud["semtitre"] = sem["titremois"]
-    etud["nowdmy"] = time.strftime("%d/%m/%Y")
-    etud["operation_name"] = operation_name
+    nowdmy = time.strftime("%d/%m/%Y")
     #
     header = html_sco_header.sco_header(
-        page_title="%(operation_name)s de  %(nomprenom)s (du semestre %(semtitre)s)"
-        % etud,
+        page_title=f"""{operation_name} de {etud.nomprenom} (du semestre {formsemestre.titre_mois()})"""
     )
-    H = [
-        '<h2><font color="#FF0000">%(operation_name)s de</font> %(nomprenom)s (semestre %(semtitre)s)</h2><p>'
-        % etud
-    ]
-    H.append(
-        """<form action="%s" method="get">
-    <b>Date de la %s (J/M/AAAA):&nbsp;</b>
+    return f"""
+    {header}
+    <h2><font color="#FF0000">{operation_name} de</font> {etud.nomprenom} ({formsemestre.titre_mois()})</h2>
+    
+    <form action="{operation_method}" method="get">
+        <div><b>Date de la {operation_name.lower()} (J/M/AAAA):&nbsp;</b>
+            <input type="text" name="event_date" width=20 value="{nowdmy}">
+        </div>
+        <input type="hidden" name="etudid" value="{etudid}">
+        <input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
+        <div class="vertical_spacing_but"><input type="submit" value="Confirmer"></div>
+    </form>
+    {html_sco_header.sco_footer()}
     """
-        % (operation_method, operation_name.lower())
-    )
-    H.append(
-        """
-<input type="text" name="event_date" width=20 value="%(nowdmy)s">
-<input type="hidden" name="etudid" value="%(etudid)s">
-<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s">
-<p>
-<input type="submit" value="Confirmer">
-</form>"""
-        % etud
-    )
-    return header + "\n".join(H) + html_sco_header.sco_footer()
 
 
-@bp.route("/doDemEtudiant")
+@bp.route("/do_dem_etudiant")
 @scodoc
 @permission_required(Permission.ScoEtudInscrit)
 @scodoc7func
-def doDemEtudiant(etudid, formsemestre_id, event_date=None):
+def do_dem_etudiant(etudid, formsemestre_id, event_date=None):
     "Déclare la démission d'un etudiant dans le semestre"
     return _do_dem_or_def_etud(
         etudid,
         formsemestre_id,
         event_date=event_date,
-        etat_new="D",
-        operation_method="demEtudiant",
+        etat_new=scu.DEMISSION,
+        operation_method="dem_etudiant",
         event_type="DEMISSION",
     )
 
 
-@bp.route("/doDefEtudiant")
+@bp.route("/do_def_etudiant")
 @scodoc
 @permission_required(Permission.ScoEtudInscrit)
 @scodoc7func
-def doDefEtudiant(etudid, formsemestre_id, event_date=None):
+def do_def_etudiant(etudid, formsemestre_id, event_date=None):
     "Déclare la défaillance d'un etudiant dans le semestre"
     return _do_dem_or_def_etud(
         etudid,
@@ -1165,7 +1158,7 @@ def _do_dem_or_def_etud(
     etudid,
     formsemestre_id,
     event_date=None,
-    etat_new="D",  # 'D' or DEF
+    etat_new=scu.DEMISSION,  # DEMISSION or DEF
     operation_method="demEtudiant",
     event_type="DEMISSION",
     redirect=True,
@@ -1175,7 +1168,7 @@ def _do_dem_or_def_etud(
         etudid,
         formsemestre_id,
         event_date=event_date,
-        etat_new=etat_new,  # 'D' or DEF
+        etat_new=etat_new,  # DEMISSION or DEF
         operation_method=operation_method,
         event_type=event_type,
     )
@@ -1185,11 +1178,11 @@ def _do_dem_or_def_etud(
         )
 
 
-@bp.route("/doCancelDem", methods=["GET", "POST"])
+@bp.route("/do_cancel_dem", methods=["GET", "POST"])
 @scodoc
 @permission_required(Permission.ScoEtudInscrit)
 @scodoc7func
-def doCancelDem(etudid, formsemestre_id, dialog_confirmed=False, args=None):
+def do_cancel_dem(etudid, formsemestre_id, dialog_confirmed=False, args=None):
     "Annule une démission"
     return _do_cancel_dem_or_def(
         etudid,
@@ -1197,18 +1190,18 @@ def doCancelDem(etudid, formsemestre_id, dialog_confirmed=False, args=None):
         dialog_confirmed=dialog_confirmed,
         args=args,
         operation_name="démission",
-        etat_current="D",
-        etat_new="I",
+        etat_current=scu.DEMISSION,
+        etat_new=scu.INSCRIT,
         operation_method="cancelDem",
         event_type="DEMISSION",
     )
 
 
-@bp.route("/doCancelDef", methods=["GET", "POST"])
+@bp.route("/do_cancel_def", methods=["GET", "POST"])
 @scodoc
 @permission_required(Permission.ScoEtudInscrit)
 @scodoc7func
-def doCancelDef(etudid, formsemestre_id, dialog_confirmed=False, args=None):
+def do_cancel_def(etudid, formsemestre_id, dialog_confirmed=False, args=None):
     "Annule la défaillance de l'étudiant"
     return _do_cancel_dem_or_def(
         etudid,
@@ -1217,8 +1210,8 @@ def doCancelDef(etudid, formsemestre_id, dialog_confirmed=False, args=None):
         args=args,
         operation_name="défaillance",
         etat_current=sco_codes_parcours.DEF,
-        etat_new="I",
-        operation_method="cancelDef",
+        etat_new=scu.INSCRIT,
+        operation_method="cancel_def",
         event_type="DEFAILLANCE",
     )
 
@@ -1229,30 +1222,30 @@ def _do_cancel_dem_or_def(
     dialog_confirmed=False,
     args=None,
     operation_name="",  # "démission" ou "défaillance"
-    etat_current="D",
-    etat_new="I",
-    operation_method="cancelDem",
+    etat_current=scu.DEMISSION,
+    etat_new=scu.INSCRIT,
+    operation_method="cancel_dem",
     event_type="DEMISSION",
 ):
-    "Annule une demission ou une défaillance"
+    "Annule une démission ou une défaillance"
+    etud: Identite = Identite.query.filter_by(
+        id=etudid, dept_id=g.scodoc_dept_id
+    ).first_or_404()
+    formsemestre: FormSemestre = FormSemestre.query.filter_by(
+        id=formsemestre_id, dept_id=g.scodoc_dept_id
+    ).first_or_404()
     # check lock
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    if not sem["etat"]:
+    if not formsemestre.etat:
         raise ScoValueError("Modification impossible: semestre verrouille")
     # verif
-    info = sco_etud.get_etud_info(etudid, filled=True)[0]
-    ok = False
-    for i in info["ins"]:
-        if i["formsemestre_id"] == formsemestre_id:
-            if i["etat"] != etat_current:
-                raise ScoValueError("etudiant non %s !" % operation_name)
-            ok = True
-            break
-    if not ok:
-        raise ScoValueError("etudiant non inscrit ???")
+    if formsemestre_id not in (inscr.formsemestre_id for inscr in etud.inscriptions()):
+        raise ScoValueError("étudiant non inscrit dans ce semestre !")
+    if etud.inscription_etat(formsemestre_id) != etat_current:
+        raise ScoValueError(f"etudiant non {operation_name} !")
+
     if not dialog_confirmed:
         return scu.confirm_dialog(
-            "<p>Confirmer l'annulation de la %s ?</p>" % operation_name,
+            f"<p>Confirmer l'annulation de la {operation_name} ?</p>",
             dest_url="",
             cancel_url=url_for(
                 "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
@@ -1260,25 +1253,22 @@ def _do_cancel_dem_or_def(
             parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
         )
     #
-    ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
-        {"etudid": etudid, "formsemestre_id": formsemestre_id}
-    )[0]
-    if ins["etat"] != etat_current:
-        raise ScoException("etudiant non %s !!!" % etat_current)  # obviously a bug
-    ins["etat"] = etat_new
-    cnx = ndb.GetDBConnexion()
-    sco_formsemestre_inscriptions.do_formsemestre_inscription_edit(
-        args=ins, formsemestre_id=formsemestre_id
-    )
-    logdb(cnx, method=operation_method, etudid=etudid)
-    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-    cursor.execute(
-        "delete from scolar_events where etudid=%(etudid)s and formsemestre_id=%(formsemestre_id)s and event_type='"
-        + event_type
-        + "'",
-        {"etudid": etudid, "formsemestre_id": formsemestre_id},
+    inscr = next(
+        inscr
+        for inscr in etud.inscriptions()
+        if inscr.formsemestre_id == formsemestre_id
     )
-    cnx.commit()
+    inscr.etat = etat_new
+    db.session.add(inscr)
+    Scolog.logdb(method=operation_method, etudid=etudid)
+    # Efface les évènements
+    for event in ScolarEvent.query.filter_by(
+        etudid=etudid, formsemestre_id=formsemestre_id, event_type=event_type
+    ):
+        db.session.delete(event)
+
+    db.session.commit()
+    flash(f"{operation_name} annulée.")
     return flask.redirect(
         url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
     )