diff --git a/app/forms/assiduite/ajout_assiduite_etud.py b/app/forms/assiduite/ajout_assiduite_etud.py
index 8c3423accf36f2e7bdece4aa075aa057188c9c31..302f73776216762af9e748b7f522b6cb91c0ecda 100644
--- a/app/forms/assiduite/ajout_assiduite_etud.py
+++ b/app/forms/assiduite/ajout_assiduite_etud.py
@@ -62,6 +62,11 @@ class AjoutAssiOrJustForm(FlaskForm):
         if field:
             field.errors.append(err_msg)
 
+    def disable_all(self):
+        "Disable all fields"
+        for field in self:
+            field.render_kw = {"disabled": True}
+
     date_debut = StringField(
         "Date de début",
         validators=[validators.Length(max=10)],
@@ -175,36 +180,3 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
         validators=[DataRequired(message="This field is required.")],
     )
     fichiers = MultipleFileField(label="Ajouter des fichiers")
-
-
-class ChoixDateForm(FlaskForm):
-    """
-    Formulaire de choix de date
-    (utilisé par la page de choix de date
-    si la date courante n'est pas dans le semestre)
-    """
-
-    def __init__(self, *args, **kwargs):
-        "Init form, adding a filed for our error messages"
-        super().__init__(*args, **kwargs)
-        self.ok = True
-        self.error_messages: list[str] = []  # used to report our errors
-
-    def set_error(self, err_msg, field=None):
-        "Set error message both in form and field"
-        self.ok = False
-        self.error_messages.append(err_msg)
-        if field:
-            field.errors.append(err_msg)
-
-    date = StringField(
-        "Date",
-        validators=[validators.Length(max=10)],
-        render_kw={
-            "class": "datepicker",
-            "size": 10,
-            "id": "date",
-        },
-    )
-    submit = SubmitField("Enregistrer")
-    cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
diff --git a/app/forms/assiduite/edit_assiduite_etud.py b/app/forms/assiduite/edit_assiduite_etud.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca284c0148c9670510c92998725080f620b5f5f0
--- /dev/null
+++ b/app/forms/assiduite/edit_assiduite_etud.py
@@ -0,0 +1,58 @@
+""" """
+
+from flask_wtf import FlaskForm
+from wtforms import SelectField, RadioField, TextAreaField, validators, SubmitField
+from app.scodoc.sco_utils import EtatAssiduite
+
+
+class EditAssiForm(FlaskForm):
+    """
+    Formulaire de modification d'une assiduité
+    """
+
+    def __init__(self, *args, **kwargs):
+        "Init form, adding a filed for our error messages"
+        super().__init__(*args, **kwargs)
+        self.ok = True
+        self.error_messages: list[str] = []  # used to report our errors
+
+    def set_error(self, err_msg, field=None):
+        "Set error message both in form and field"
+        self.ok = False
+        self.error_messages.append(err_msg)
+        if field:
+            field.errors.append(err_msg)
+
+    def disable_all(self):
+        "Disable all fields"
+        for field in self:
+            field.render_kw = {"disabled": True}
+
+    assi_etat = RadioField(
+        "État:",
+        choices=[
+            (EtatAssiduite.ABSENT.value, EtatAssiduite.ABSENT.version_lisible()),
+            (EtatAssiduite.RETARD.value, EtatAssiduite.RETARD.version_lisible()),
+            (EtatAssiduite.PRESENT.value, EtatAssiduite.PRESENT.version_lisible()),
+        ],
+        default="absent",
+        validators=[
+            validators.DataRequired("spécifiez le type d'évènement à signaler"),
+        ],
+    )
+    modimpl = SelectField(
+        "Module",
+        choices={},  # will be populated dynamically
+    )
+    description = TextAreaField(
+        "Description",
+        render_kw={
+            "id": "description",
+            "cols": 75,
+            "rows": 4,
+            "maxlength": 500,
+        },
+    )
+
+    submit = SubmitField("Enregistrer")
+    cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
diff --git a/app/models/assiduites.py b/app/models/assiduites.py
index 241e44aa636f1c34c6ed8d754477f02fa454d489..b741005e4269058e9d3f4e4c292e365c78cb845e 100644
--- a/app/models/assiduites.py
+++ b/app/models/assiduites.py
@@ -360,6 +360,16 @@ class Assiduite(ScoDocModel):
 
         return "Module non spécifié" if traduire else None
 
+    def get_moduleimpl_id(self) -> int | str | None:
+        """
+        Retourne le ModuleImpl associé à l'assiduité
+        """
+        if self.moduleimpl_id is not None:
+            return self.moduleimpl_id
+        if self.external_data is not None and "module" in self.external_data:
+            return self.external_data["module"]
+        return None
+
     def get_saisie(self) -> str:
         """
         retourne le texte "saisie le <date> par <User>"
@@ -395,6 +405,14 @@ class Assiduite(ScoDocModel):
         if force:
             raise ScoValueError("Module non renseigné")
 
+    @classmethod
+    def get_assiduite(cls, assiduite_id: int) -> "Assiduite":
+        """Assiduité ou 404, cherche uniquement dans le département courant"""
+        query = Assiduite.query.filter_by(id=assiduite_id)
+        if g.scodoc_dept:
+            query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
+        return query.first_or_404()
+
 
 class Justificatif(ScoDocModel):
     """
diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js
index e796923f2b1cc81e3723f7ac23ec829b9787d4cd..91c651e37a8b1c74008d868fa879cb3a2f3f2093 100644
--- a/app/static/js/assiduites.js
+++ b/app/static/js/assiduites.js
@@ -870,21 +870,12 @@ function setupAssiduiteBubble(el, assiduite) {
   const infos = document.createElement("a");
   infos.className = "";
   infos.textContent = `ℹ️`;
-  infos.title = "Cliquez pour plus d'informations";
+  infos.title = "Détails / Modifier";
   infos.target = "_blank";
-  infos.href = `tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assiduite.assiduite_id}`;
-
-  // Ajout d'un lien pour modifier l'assiduité
-  const modifs = document.createElement("a");
-  modifs.className = "";
-  modifs.textContent = `📝`;
-  modifs.title = "Cliquez pour modifier l'assiduité";
-  modifs.target = "_blank";
-  modifs.href = `tableau_assiduite_actions?type=assiduite&action=modifier&obj_id=${assiduite.assiduite_id}`;
+  infos.href = `edit_assiduite_etud/${assiduite.assiduite_id}`;
 
   const actionsDiv = document.createElement("div");
   actionsDiv.className = "assiduite-actions";
-  actionsDiv.appendChild(modifs);
   actionsDiv.appendChild(infos);
   bubble.appendChild(actionsDiv);
 
diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py
index c0c7dbbba76b397fbdbde850bd70e1c00b1f34fd..7b0204b92bc52007317a368bb37cbe7d005085f8 100644
--- a/app/tables/liste_assiduites.py
+++ b/app/tables/liste_assiduites.py
@@ -600,33 +600,22 @@ class RowAssiJusti(tb.Row):
         url: str
         html: list[str] = []
 
-        # Détails
-        url = url_for(
-            "assiduites.tableau_assiduite_actions",
-            type=self.ligne["type"],
-            action="details",
-            obj_id=self.ligne["obj_id"],
-            scodoc_dept=g.scodoc_dept,
-        )
-        html.append(f'<a title="Détails" href="{url}">ℹ️</a>')
-
-        # Modifier
         if self.ligne["type"] == "justificatif":
+            # Détails/Modifier assiduité
             url = url_for(
                 "assiduites.edit_justificatif_etud",
                 justif_id=self.ligne["obj_id"],
                 scodoc_dept=g.scodoc_dept,
-                back_url=request.url,
             )
+            html.append(f'<a title="Détails/Modifier" href="{url}">ℹ️</a>')
         else:
+            # Détails/Modifier assiduité
             url = url_for(
-                "assiduites.tableau_assiduite_actions",
-                type=self.ligne["type"],
-                action="modifier",
-                obj_id=self.ligne["obj_id"],
+                "assiduites.edit_assiduite_etud",
+                assiduite_id=self.ligne["obj_id"],
                 scodoc_dept=g.scodoc_dept,
             )
-        html.append(f'<a title="Modifier" href="{url}">📝</a>')
+            html.append(f'<a title="Détails/Modifier" href="{url}">ℹ️</a>')
 
         # Supprimer
         url = url_for(
diff --git a/app/templates/assiduites/pages/ajout_justificatif_etud.j2 b/app/templates/assiduites/pages/ajout_justificatif_etud.j2
index 221f3b496132cedb6905fc4e3d0031f39e596692..a6527203b220a307d6d70cd2a08d764ac7451900 100644
--- a/app/templates/assiduites/pages/ajout_justificatif_etud.j2
+++ b/app/templates/assiduites/pages/ajout_justificatif_etud.j2
@@ -44,11 +44,33 @@ div.submit > input {
 </style>
 <div class="tab-content">
     <h2>{{title|safe}}</h2>
-
+    {% if readonly %}
+    <h3 class="rouge">Vous n'avez pas la permission de modifier ce justificatif</h3>
+    {% endif %}
     {% if justif %}
+        <div class="informations">
+        
         <div class="info-saisie">
-            Saisie par {{justif.user.get_prenomnom() if justif.user else "inconnu"}}
-            le {{justif.entry_date.strftime(scu.DATEATIME_FMT) if justif.entry_date else "?"}}
+            <span>Saisie par {{justif.saisie_par}} le {{justif.entry_date}}</span>
+        </div>
+
+        <div class="info-row">
+            <span class="info-label">Assiduités concernées: </span>
+            {% if justif.justification.assiduites %}
+            <ul>
+                {% for assi in justif.justification.assiduites %}
+                <li><a href="{{url_for('assiduites.edit_assiduite_etud',
+                    assiduite_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)
+                    }}" target="_blank">{{assi.etat}} du {{assi.date_debut}} au
+                    {{assi.date_fin}}</a>
+                </li>
+                {% endfor %}
+            </ul>
+            {% else %}
+            <span class="text">Aucune</span>
+            {% endif %}
+        </div>
+        
         </div>
     {% endif %}
 
@@ -110,7 +132,9 @@ div.submit > input {
                 {% for filename in filenames %}
                     <li><span data-justif_id="{{justif.id}}" class="suppr_fichier_just"
                     >{{scu.icontag("delete_img", alt="supprimer", title="Supprimer")|safe}}</span>
-                    {{filename}}</li>
+                    <a href="{{url_for('apiweb.justif_export',justif_id=justif.justif_id,
+                            filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
+                    </li>
                 {% endfor %}
                 </ul>
             {% endif %}
@@ -126,11 +150,28 @@ div.submit > input {
         <span class="help" style="margin-left: 12px;">laisser vide pour date courante</span>
         {{ render_field_errors(form, 'entry_date') }}
 
+        {% if readonly == False %}
         {# Submit #}
         <div class="submit">
         {{ form.submit }} {{ form.cancel }}
         </div>
 
+        <div class="info-row">
+            <a
+                style="color:red;"
+                href="{{url_for(
+                'assiduites.tableau_assiduite_actions',
+                type='justificatif',
+                action='supprimer',
+                obj_id=justif.justif_id,
+                scodoc_dept=g.scodoc_dept,
+            )}}"
+            >Supprimer le justificatif</a>
+        </div>
+        {% endif %}
+
+        
+
     </fieldset>
     </form>
     </section>
diff --git a/app/templates/assiduites/pages/calendrier_assi_etud.j2 b/app/templates/assiduites/pages/calendrier_assi_etud.j2
index d782229443a9f806265523da437807088c2039a4..b4fa38547c7f024f1ae6ca07fb6e750847f7f6ce 100644
--- a/app/templates/assiduites/pages/calendrier_assi_etud.j2
+++ b/app/templates/assiduites/pages/calendrier_assi_etud.j2
@@ -242,7 +242,7 @@ Calendrier de l'assiduité
         document.querySelectorAll('[assi_id]').forEach((el, i) => {
             el.addEventListener('click', () => {
                 const assi_id = el.getAttribute('assi_id');
-                window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
+                window.open(`${SCO_URL}Assiduites/edit_assiduite_etud/${assi_id}`);
             })
         });
     </script>
diff --git a/app/templates/assiduites/pages/edit_assiduite_etud.j2 b/app/templates/assiduites/pages/edit_assiduite_etud.j2
new file mode 100644
index 0000000000000000000000000000000000000000..8b0930e879b2056e284c3dcb7e36c24d7d111d30
--- /dev/null
+++ b/app/templates/assiduites/pages/edit_assiduite_etud.j2
@@ -0,0 +1,165 @@
+{# Ajout d'une "assiduité" sur un étudiant #}
+
+{% extends "sco_page.j2" %}
+{% import 'wtf.j2' as wtf %}
+
+
+{% block styles %}
+{{super()}}
+<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
+
+<style>
+    .info-row {
+        margin-top: 12px;
+    }
+
+    .info-label {
+        font-weight: bold;
+    }
+    #assi_etat{
+        list-style: none;
+    }
+
+    .info-etat {
+        font-size: 110%;
+        font-weight: bold;
+        background-color: rgb(253, 234, 210);
+        border: 1px solid grey;
+        border-radius: 4px;
+        padding: 4px;
+    }
+
+    .info-saisie {
+        margin-top: 12px;
+        margin-bottom: 12px;
+        font-style: italic;
+    }
+</style>
+{% endblock %}
+
+{% block app_content %}
+<div class="tab-content">
+    <h2>Détails Assiduité concernant {{etud.html_link_fiche()|safe}}</h2>
+
+    <div id="informations">
+        <div class="info-saisie">
+            <span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
+        </div>
+        <div class="info-row">
+            <span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b>
+        </div>
+        <div class="info-row">
+            <span class="info-label">Module :</span> {{objet.module}}
+        </div>
+        <div class="info-row">
+            <span class="info-label">État de l'assiduité :</span><span class="info-etat">{{objet.etat}}</span>
+        </div>
+        <div class="info-row">
+            <span class="info-label">Description:</span>
+            {% if objet.description != "" and objet.description is not None %}
+            <span class="text">{{objet.description}}</span>
+            {% else %}
+            <span class="text fontred">Pas de description</span>
+            {% endif %}
+            </span>
+        </div>
+        {# Affichage des justificatifs si assiduité justifiée #}
+        {% if objet.etat != "Présence" %}
+        <div class="info-row">
+            <span class="info-label">Justifiée: </span>
+            {% if objet.justification.est_just %}
+            <span class="text">Oui</span>
+            {% else %}
+            <span class="text fontred">Non</span>
+            {% if not objet.justification.justificatifs %}
+                <a
+                    href="{{url_for(
+                    'assiduites.tableau_assiduite_actions',
+                    type='assiduite',
+                    action='justifier',
+                    obj_id=objet.assiduite_id,
+                    scodoc_dept=g.scodoc_dept,
+                )}}"
+                >Justifier l'assiduité</a>
+            {% endif %}
+            {% endif %}
+        </div>
+        <div class="info-row">
+        {% if not objet.justification.justificatifs %}
+            <span class="text info-label">Pas de justificatif associé</span>
+            {% else %}
+            <span class="text info-label">Justificatifs associés:</span>
+            <ul>
+                {% for justi in objet.justification.justificatifs %}
+                <li>
+                    <a href="{{url_for('assiduites.edit_justificatif_etud',
+                    justif_id=justi.justif_id,scodoc_dept=g.scodoc_dept)}}"
+                        target="_blank" rel="noopener noreferrer" style="{{'color:red;' if justi.etat != 'Valide'}}">Justificatif {{justi.etat}} du {{justi.date_debut}} au
+                        {{justi.date_fin}}</a>
+                </li>
+                {% endfor %}
+            </ul>
+            {% endif %}
+        </div>
+        {% endif %}
+    </div>
+
+    {% if readonly != True %}
+    <h2 style="margin-top: 24px;">Modification de l'assiduité</h2>
+    {% for err_msg in form.error_messages %}
+    <div class="wtf-error-messages">
+        {{ err_msg }}
+    </div>
+    {% endfor %}
+
+    <form id="edit-assiduite-form" method="post">
+        {{ form.hidden_tag() }}
+        {# Type d'évènement #}
+        <div class="radio-assi_etat">
+            {{ form.assi_etat.label }}
+            {{ form.assi_etat() }}
+        </div>
+        {# Menu module #}
+        <div class="select-module">
+            {{ form.modimpl.label }}&nbsp;:
+            {{ form.modimpl }}
+            {{ render_field_errors(form, 'modimpl') }}
+        </div>
+        {# Description #}
+        <div>
+            <div>{{ form.description.label }}</div>
+            {{ form.description() }}
+            {{ render_field_errors(form, 'description') }}
+        </div>
+        {# Submit #}
+        <div class="submit info-row">
+            {{ form.submit }} {{ form.cancel }}
+        </div>
+
+        
+    </form>
+    <div class="info-row">
+        <a
+            style="color:red;"
+            href="{{url_for(
+            'assiduites.tableau_assiduite_actions',
+            type='assiduite',
+            action='supprimer',
+            obj_id=objet.assiduite_id,
+            scodoc_dept=g.scodoc_dept,
+        )}}"
+        >Supprimer l'assiduité</a>
+    </div>
+    {% else %}
+    <h3 class="rouge">Vous n'avez pas la permission de modifier cette assiduité</h3>
+    {% endif %}
+
+</div>
+
+{% endblock app_content %}
+
+{% block scripts %}
+{{ super() }}
+<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
+{% include "sco_timepicker.j2" %}
+{% endblock scripts %}
\ No newline at end of file
diff --git a/app/views/assiduites.py b/app/views/assiduites.py
index e71a3246961fd17e21e747af68b3e151192c0adb..b98d61ecd01c8e50012493ea8b2ed0036b5b5d13 100644
--- a/app/views/assiduites.py
+++ b/app/views/assiduites.py
@@ -36,6 +36,7 @@ from flask_login import current_user
 from flask_sqlalchemy.query import Query
 
 from markupsafe import Markup
+from werkzeug.exceptions import HTTPException
 
 from app import db, log
 from app.comp import res_sem
@@ -48,8 +49,8 @@ from app.forms.assiduite.ajout_assiduite_etud import (
     AjoutAssiOrJustForm,
     AjoutAssiduiteEtudForm,
     AjoutJustificatifEtudForm,
-    ChoixDateForm,
 )
+from app.forms.assiduite.edit_assiduite_etud import EditAssiForm
 from app.models import (
     Assiduite,
     Departement,
@@ -538,10 +539,8 @@ def _record_assiduite_etud(
             assi: Assiduite = conflits.first()
 
             lien: str = url_for(
-                "assiduites.tableau_assiduite_actions",
-                type="assiduite",
-                action="details",
-                obj_id=assi.assiduite_id,
+                "assiduites.edit_assiduite_etud",
+                assiuite_id=assi.assiduite_id,
                 scodoc_dept=g.scodoc_dept,
             )
 
@@ -612,7 +611,7 @@ def bilan_etud():
 
 @bp.route("/edit_justificatif_etud/<int:justif_id>", methods=["GET", "POST"])
 @scodoc
-@permission_required(Permission.AbsChange)
+@permission_required(Permission.ScoView)
 def edit_justificatif_etud(justif_id: int):
     """
     Edition d'un justificatif.
@@ -624,8 +623,19 @@ def edit_justificatif_etud(justif_id: int):
     Returns:
         str: l'html généré
     """
-    justif = Justificatif.get_justificatif(justif_id)
+    try:
+        justif = Justificatif.get_justificatif(justif_id)
+    except HTTPException:
+        flash("Justificatif invalide")
+        return redirect(url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept))
+
+    readonly = not current_user.has_permission(Permission.AbsChange)
+
     form = AjoutJustificatifEtudForm(obj=justif)
+
+    if readonly:
+        form.disable_all()
+
     # Set the default value for the etat field
     if request.method == "GET":
         form.date_debut.data = justif.date_debut.strftime(scu.DATE_FMT)
@@ -652,7 +662,9 @@ def edit_justificatif_etud(justif_id: int):
     )
 
     if form.validate_on_submit():
-        if form.cancel.data:  # cancel button
+        if form.cancel.data or not current_user.has_permission(
+            Permission.AbsChange
+        ):  # cancel button
             return redirect(redirect_url)
         if _record_justificatif_etud(justif.etudiant, form, justif):
             return redirect(redirect_url)
@@ -667,12 +679,13 @@ def edit_justificatif_etud(justif_id: int):
         etud=justif.etudiant,
         filenames=filenames,
         form=form,
-        justif=justif,
+        justif=_preparer_objet("justificatif", justif),
         nb_files=nb_files,
         title=f"Modification justificatif absence de {justif.etudiant.html_link_fiche()}",
         redirect_url=redirect_url,
         sco=ScoData(justif.etudiant),
         scu=scu,
+        readonly=not current_user.has_permission(Permission.AbsChange),
     )
 
 
@@ -1672,20 +1685,18 @@ def _preparer_objet(
 
         # Gestion justification
 
-        if not objet.est_just:
-            objet_prepare["justification"] = {"est_just": False}
-        else:
-            objet_prepare["justification"] = {"est_just": True, "justificatifs": []}
+        objet_prepare["justification"] = {
+            "est_just": objet.est_just,
+            "justificatifs": [],
+        }
 
-            if not sans_gros_objet:
-                justificatifs: list[int] = get_assiduites_justif(
-                    objet.assiduite_id, False
+        if not sans_gros_objet:
+            justificatifs: list[int] = get_assiduites_justif(objet.assiduite_id, False)
+            for justi_id in justificatifs:
+                justi: Justificatif = Justificatif.query.get(justi_id)
+                objet_prepare["justification"]["justificatifs"].append(
+                    _preparer_objet("justificatif", justi, sans_gros_objet=True)
                 )
-                for justi_id in justificatifs:
-                    justi: Justificatif = Justificatif.query.get(justi_id)
-                    objet_prepare["justification"]["justificatifs"].append(
-                        _preparer_objet("justificatif", justi, sans_gros_objet=True)
-                    )
 
     else:  # objet == "justificatif"
         justif: Justificatif = objet
@@ -1698,9 +1709,8 @@ def _preparer_objet(
 
         objet_prepare["justification"] = {"assiduites": [], "fichiers": {}}
         if not sans_gros_objet:
-            assiduites: list[int] = scass.justifies(justif)
-            for assi_id in assiduites:
-                assi: Assiduite = Assiduite.query.get(assi_id)
+            assiduites: list[Assiduite] = justif.get_assiduites()
+            for assi in assiduites:
                 objet_prepare["justification"]["assiduites"].append(
                     _preparer_objet("assiduite", assi, sans_gros_objet=True)
                 )
@@ -2152,6 +2162,106 @@ def signal_assiduites_hebdo():
     )
 
 
+@bp.route("edit_assiduite_etud/<int:assiduite_id>", methods=["GET", "POST"])
+@scodoc
+@permission_required(Permission.ScoView)
+def edit_assiduite_etud(assiduite_id: int):
+    """
+    Page affichant les détails d'une assiduité
+    Si le current_user alors la page propose un formulaire de modification
+    """
+    try:
+        assi: Assiduite = Assiduite.get_assiduite(assiduite_id=assiduite_id)
+    except HTTPException:
+        flash("Assiduité invalide")
+        return redirect(url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept))
+
+    etud: Identite = assi.etudiant
+    formsemestre: FormSemestre = assi.get_formsemestre()
+
+    readonly: bool = not current_user.has_permission(Permission.AbsChange)
+
+    form: EditAssiForm = EditAssiForm(request.form)
+    if readonly:
+        form.disable_all()
+
+    # peuplement moduleimpl_select
+    modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
+    choices: OrderedDict = OrderedDict()
+    choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
+
+    # indique le nom du semestre dans le menu (optgroup)
+    group_name: str = formsemestre.titre_annee()
+    choices[group_name] = [
+        (m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}")
+        for m in modimpls_by_formsemestre[formsemestre.id]
+        if m.module.ue.type == UE_STANDARD
+    ]
+
+    choices.move_to_end("", last=False)
+    form.modimpl.choices = choices
+
+    # Vérification formulaire
+    if form.validate_on_submit():
+        if form.cancel.data:  # cancel button
+            return redirect(request.referrer)
+
+        # vérification des valeurs
+
+        # Gestion de l'état
+        etat = form.assi_etat.data
+        try:
+            etat = int(etat)
+            etat = scu.EtatAssiduite.inverse().get(etat, None)
+        except ValueError:
+            etat = None
+
+        if etat is None:
+            form.error_messages.append("État invalide")
+            form.ok = False
+
+        description = form.description.data or ""
+        description = description.strip()
+        moduleimpl_id = form.modimpl.data or -1
+        if isinstance(moduleimpl_id, int):
+            try:
+                ModuleImpl.get_moduleimpl(moduleimpl_id)
+            except ValueError:
+                form.error_messages.append("Module invalide")
+                moduleimpl_id = -1
+                form.ok = False
+
+        if form.ok:
+            assi.etat = etat
+            assi.description = description
+            if moduleimpl_id != -1:
+                assi.set_moduleimpl(moduleimpl_id)
+
+            db.session.add(assi)
+            db.session.commit()
+
+            scass.simple_invalidate_cache(assi.to_dict(format_api=True), assi.etudid)
+
+            flash("enregistré")
+            return redirect(request.referrer)
+
+    # Remplissage du formulaire
+    form.assi_etat.data = str(assi.etat)
+    form.description.data = assi.description
+    moduleimpl_id: int | str | None = assi.get_moduleimpl_id() or ""
+    form.modimpl.data = str(moduleimpl_id)
+
+    return render_template(
+        "assiduites/pages/edit_assiduite_etud.j2",
+        etud=etud,
+        sco=ScoData(etud, formsemestre=formsemestre),
+        form=form,
+        readonly=readonly,
+        objet=_preparer_objet("assiduite", assi),
+        title=f"Assiduité {etud.nom_short}",
+    )
+
+
 def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
     """Génère la liste des assiduités d'un étudiant pour le bulletin mail"""