diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index 386f900304d26810bbad21e92f0aab6a9fffdb63..eeac44d88533f2e24010eb2967c1b7da973fd962 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -80,9 +80,11 @@ def _menu_scolarite(
             title="Semestre verrouillé",
         )
         return lockicon  # no menu
-    if not authuser.has_permission(
-        Permission.EtudInscrit
-    ) and not authuser.has_permission(Permission.EtudChangeGroups):
+    if (
+        not authuser.has_permission(Permission.EtudInscrit)
+        and not authuser.has_permission(Permission.EtudChangeGroups)
+        and authuser.id not in [resp.id for resp in formsemestre.responsables]
+    ):
         return ""  # no menu
 
     args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
@@ -106,6 +108,13 @@ def _menu_scolarite(
         and authuser.has_permission(Permission.EtudInscrit)
         and not locked
     )
+    note_enabled = etat_inscription == "I" and (
+        etat_inscription != scu.DEMISSION
+        and (
+            authuser.has_permission(Permission.EditAllNotes)
+            or (authuser.id in [resp.id for resp in formsemestre.responsables])
+        )
+    )
     items = [
         {
             "title": dem_title,
@@ -161,6 +170,12 @@ def _menu_scolarite(
             "args": {"etudid": etudid},
             "enabled": authuser.has_permission(Permission.EtudInscrit),
         },
+        {
+            "title": "Gérer les notes",
+            "endpoint": "notes.form_saisie_notes_par_etu",
+            "args": {"etu_id": etudid, "semestre_id": formsemestre.id},
+            "enabled": note_enabled,
+        },
     ]
 
     return htmlutils.make_menu(
@@ -215,9 +230,9 @@ def fiche_etud(etudid=None):
         info[
             "modifadresse"
         ] = f"""<a class="stdlink" href="{
-                url_for("scolar.form_change_coordonnees",
-                    scodoc_dept=g.scodoc_dept, etudid=etudid)
-            }">modifier adresse</a>"""
+        url_for("scolar.form_change_coordonnees",
+                scodoc_dept=g.scodoc_dept, etudid=etudid)
+        }">modifier adresse</a>"""
     else:
         info["modifadresse"] = ""
 
@@ -260,8 +275,8 @@ def fiche_etud(etudid=None):
                 grlinks.append(
                     f"""<a class="discretelink" href="{
                     url_for('scolar.groups_lists',
-                    scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
-                }" title="Liste du groupe {gr_name}">{gr_name}</a>
+                            scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
+                    }" title="Liste du groupe {gr_name}">{gr_name}</a>
                 """
                 )
             grlink = ", ".join(grlinks)
@@ -289,8 +304,8 @@ def fiche_etud(etudid=None):
             else f"""
             <span class="link_bul_pdf">
             <a class="stdlink" href="{
-                url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
-                }">Tous les bulletins</a>
+            url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
+            }">Tous les bulletins</a>
             </span>
             """
         )
@@ -301,16 +316,16 @@ def fiche_etud(etudid=None):
             ] += f"""
             <span class="link_bul_pdf">
             <a class="stdlink" href="{
-                url_for("notes.validation_rcues",
+            url_for("notes.validation_rcues",
                     scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
-                }">Visualiser les compétences BUT</a>
+            }">Visualiser les compétences BUT</a>
             </span>
             """
         info["link_inscrire_ailleurs"] = (
             f"""<span class="link_bul_pdf"><a class="stdlink" href="{
-                url_for("notes.formsemestre_inscription_with_modules_form",
-                scodoc_dept=g.scodoc_dept, etudid=etudid)
-                }">Inscrire à un autre semestre</a></span>
+            url_for("notes.formsemestre_inscription_with_modules_form",
+                    scodoc_dept=g.scodoc_dept, etudid=etudid)
+            }">Inscrire à un autre semestre</a></span>
                 """
             if current_user.has_permission(Permission.EtudInscrit)
             else ""
@@ -320,26 +335,26 @@ def fiche_etud(etudid=None):
             "link_inscrire_ailleurs"
         ] += f"""
                 <span class="link_bul_pdf"><a class="stdlink" href="{
-                url_for("notes.jury_delete_manual",
+        url_for("notes.jury_delete_manual",
                 scodoc_dept=g.scodoc_dept, etudid=etudid,
                 read_only=not can_edit_jury)
-                }">{'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury</a></span>
+        }">{'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury</a></span>
                 """
 
         info[
             "link_bilan_ects"
         ] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
-                url_for("notes.etud_bilan_ects",
+        url_for("notes.etud_bilan_ects",
                 scodoc_dept=g.scodoc_dept, etudid=etudid)
-                }">ECTS</a></span>"""
+        }">ECTS</a></span>"""
     else:
         # non inscrit
         l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
         if current_user.has_permission(Permission.EtudInscrit):
             l.append(
                 f"""<a href="{
-                    url_for("notes.formsemestre_inscription_with_modules_form",
-                    scodoc_dept=g.scodoc_dept, etudid=etudid)
+                url_for("notes.formsemestre_inscription_with_modules_form",
+                        scodoc_dept=g.scodoc_dept, etudid=etudid)
                 }">inscrire</a></li>"""
             )
         l.append("</b></b>")
@@ -488,10 +503,10 @@ def fiche_etud(etudid=None):
         validation_dut120_html = (
             f"""Diplôme DUT décerné
                 en&nbsp; <a class="stdlink" href="{
-                    url_for("notes.formsemestre_status",
-                        scodoc_dept=g.scodoc_dept,
-                        formsemestre_id=validation_dut120.formsemestre.id)
-                    }">S{validation_dut120.formsemestre.semestre_id}</a>
+            url_for("notes.formsemestre_status",
+                    scodoc_dept=g.scodoc_dept,
+                    formsemestre_id=validation_dut120.formsemestre.id)
+            }">S{validation_dut120.formsemestre.semestre_id}</a>
                 """
             if validation_dut120
             else ""
@@ -502,16 +517,16 @@ def fiche_etud(etudid=None):
         ] = f"""
             <div class="section_but">
                 {render_template(
-                    "but/cursus_etud.j2",
-                    cursus=but_cursus,
-                    scu=scu,
-                    validation_dut120_html=validation_dut120_html,
-                ) if but_cursus else '<span class="pb-config">problème configuration formation BUT</span>'}
+            "but/cursus_etud.j2",
+            cursus=but_cursus,
+            scu=scu,
+            validation_dut120_html=validation_dut120_html,
+        ) if but_cursus else '<span class="pb-config">problème configuration formation BUT</span>'}
                 <div class="fiche_but_col2">
                     <div class="link_validation_rcues">
                         <a class="stdlink" href="{url_for("notes.validation_rcues",
-                                            scodoc_dept=g.scodoc_dept, etudid=etudid,
-                                            formsemestre_id=last_formsemestre.id)}"
+                                                          scodoc_dept=g.scodoc_dept, etudid=etudid,
+                                                          formsemestre_id=last_formsemestre.id)}"
                             title="Visualiser les compétences BUT"
                             >
                             <img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="132px"/>
@@ -726,7 +741,7 @@ def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
 
     return {
         # infos accessibles à tous:
-        "bac_specialite": f"{etud.admission.bac or ''}{(' '+(etud.admission.specialite or '')) if etud.admission.specialite else ''}",
+        "bac_specialite": f"{etud.admission.bac or ''}{(' ' + (etud.admission.specialite or '')) if etud.admission.specialite else ''}",
         "annee_bac": etud.admission.annee_bac or "",
         # infos protégées par ViewEtudData:
         "info_lycee": info_lycee,
@@ -757,15 +772,15 @@ def get_html_annotations_list(etud: Identite) -> list[str]:
     for annot in annotations:
         del_link = (
             f"""<td class="annodel"><a href="{
-                url_for("scolar.doSuppressAnnotation",
+            url_for("scolar.doSuppressAnnotation",
                     scodoc_dept=g.scodoc_dept, etudid=etud.id, annotation_id=annot.id)}">{
-                        scu.icontag(
-                        "delete_img",
-                        border="0",
-                        alt="suppress",
-                        title="Supprimer cette annotation",
-                )
-                }</a></td>"""
+            scu.icontag(
+                "delete_img",
+                border="0",
+                alt="suppress",
+                title="Supprimer cette annotation",
+            )
+            }</a></td>"""
             if sco_permissions_check.can_suppress_annotation(annot.id)
             else ""
         )
@@ -773,7 +788,7 @@ def get_html_annotations_list(etud: Identite) -> list[str]:
         author = User.query.filter_by(user_name=annot.author).first()
         html_annotations_list.append(
             f"""<tr><td><span class="annodate">Le {
-                annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
+            annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
             par {author.get_prenomnom() if author else "?"} :
             </span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
             """
@@ -854,8 +869,8 @@ def etud_info_html(etudid, with_photo="1", debug=False):
     H = f"""<div class="etud_info_div">
     <div class="eid_left">
      <div class="eid_nom"><div><a class="stdlink" target="_blank" href="{
-         url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
-     }">{etud.nomprenom}</a></div></div>
+    url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
+    }">{etud.nomprenom}</a></div></div>
      <div class="eid_info eid_bac">Bac: <span class="eid_bac">{bac_abbrev}</span></div>
      <div class="eid_info eid_parcours">{code_cursus}</div>
     """
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index 79d55c0b65c52f39f1b96f2c46ae6b67a1449af0..ea31e95380446ac4ad6ddfc9783a45590fb32291 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -29,7 +29,6 @@ Formulaire revu en juillet 2016
 import html
 import time
 
-
 import flask
 from flask import g, render_template, url_for
 from flask_login import current_user
@@ -47,6 +46,7 @@ from app.models import (
     ModuleImpl,
     ScolarNews,
     Assiduite,
+    NotesNotes,
 )
 from app.models.etudiants import Identite
 
@@ -68,6 +68,7 @@ from app.scodoc import sco_undo_notes
 import app.scodoc.notesdb as ndb
 from app.scodoc.TrivialFormulator import TF
 import app.scodoc.sco_utils as scu
+from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_utils import json_error
 from app.scodoc.sco_utils import ModuleType
 from app.views import ScoData
@@ -111,6 +112,26 @@ def convert_note_from_string(
     return note_value, invalid
 
 
+def get_bounds(evaluation: Evaluation) -> tuple[int, int]:
+    """Calcule les notes min et max pour une évaluation."""
+    note_max = evaluation.note_max or 0.0
+    module: Module = evaluation.moduleimpl.module
+    if module.module_type in (
+        scu.ModuleType.STANDARD,
+        scu.ModuleType.RESSOURCE,
+        scu.ModuleType.SAE,
+    ):
+        if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
+            note_min, note_max = -20, 20
+        else:
+            note_min = scu.NOTES_MIN
+    elif module.module_type == ModuleType.MALUS:
+        note_min = -20.0
+    else:
+        raise ValueError("Invalid module type")  # bug
+    return note_min, note_max
+
+
 def check_notes(
     notes: list[(int, float | str)], evaluation: Evaluation
 ) -> tuple[list[tuple[int, float]], list[int], list[int], list[int], list[int]]:
@@ -128,24 +149,10 @@ def check_notes(
         etudids_non_inscrits : etudid non inscrits à ce module
                                 (ne considère pas l'inscr. au semestre)
     """
-    note_max = evaluation.note_max or 0.0
-    module: Module = evaluation.moduleimpl.module
-    if module.module_type in (
-        scu.ModuleType.STANDARD,
-        scu.ModuleType.RESSOURCE,
-        scu.ModuleType.SAE,
-    ):
-        if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
-            note_min, note_max = -20, 20
-        else:
-            note_min = scu.NOTES_MIN
-    elif module.module_type == ModuleType.MALUS:
-        note_min = -20.0
-    else:
-        raise ValueError("Invalid module type")  # bug
+    note_min, note_max = get_bounds(evaluation)
     # Vérifie inscription au module (même DEM/DEF)
     etudids_inscrits_mod = {
-        i.etudid for i in evaluation.moduleimpl.query_inscriptions()
+        i.etudid for i in evaluation.moduleimpl.query_inscriptions().all()
     }
     valid_notes = []
     etudids_invalids = []
@@ -256,7 +263,7 @@ def do_evaluation_set_missing(
             "sco_page.j2",
             content=f"""
             <h2>{diag}</h2>
-            <p><a href="{ dest_url }">
+            <p><a href="{dest_url}">
             Recommencer</a>
             </p>
             """,
@@ -313,11 +320,11 @@ def do_evaluation_set_missing(
             Revenir au formulaire de saisie des notes</a>
             </li>
             <li><a class="stdlink" href="{
-                url_for(
-                    "notes.moduleimpl_status",
-                    scodoc_dept=g.scodoc_dept,
-                    moduleimpl_id=evaluation.moduleimpl_id,
-                )}">Tableau de bord du module</a>
+        url_for(
+            "notes.moduleimpl_status",
+            scodoc_dept=g.scodoc_dept,
+            moduleimpl_id=evaluation.moduleimpl_id,
+        )}">Tableau de bord du module</a>
             </li>
         </ul>
         """,
@@ -455,11 +462,11 @@ def notes_add(
         i.etudid for i in evaluation.moduleimpl.query_inscriptions()
     }
     # Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF)
-    etudids_inscrits_sem, etudids_actifs = (
-        evaluation.moduleimpl.formsemestre.etudids_actifs()
-    )
+    (
+        etudids_inscrits_sem,
+        etudids_actifs,
+    ) = evaluation.moduleimpl.formsemestre.etudids_actifs()
     for etudid, value in notes:
-
         if check_inscription:
             _check_inscription(etudid, etudids_inscrits_sem, etudids_inscrits_mod)
 
@@ -610,7 +617,7 @@ def _record_note(
                 if do_it:
                     log(
                         f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={
-                            etudid}, oldval={oldval}"""
+                        etudid}, oldval={oldval}"""
                     )
                     cursor.execute(
                         """DELETE FROM notes_notes
@@ -652,7 +659,7 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ())
 
             <p>(vérifiez que le semestre n'est pas verrouillé et que vous
                avez l'autorisation d'effectuer cette opération)</p>
-               <p><a href="{ moduleimpl_status_url }">Continuer</a>
+               <p><a href="{moduleimpl_status_url}">Continuer</a>
             </p>
         """,
         )
@@ -762,11 +769,162 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ())
         "sco_page.j2",
         content="\n".join(H),
         title=page_title,
-        javascripts=["js/groups_view.js", "js/saisie_notes.js"],
+        javascripts=[
+            "js/groups_view.js",
+            "js/saisie_notes.js",
+            "js/saisie_notes_par_eval.js",
+        ],
         sco=ScoData(formsemestre=modimpl.formsemestre),
     )
 
 
+# US 1030 - DEB
+
+
+class DataSection:
+    """Agrège les données sur un ensemble de module (Ressources, SAEs, Standard, Malus).
+    Nécessaire pour garantir l'ordre de présentation pour les formations en apc.
+    data_modmpl: la liste des modules de la section.
+    """
+
+    def __init__(self, module_type):
+        self.name = scu.MODULE_TYPE_NAMES[module_type]
+        self.data_modimpl = []
+
+    def add_modimpl(self, modimpl):
+        self.data_modimpl.append(modimpl)
+
+
+class DataModimpl:
+    """Aggrèges les données sur un module pour un étudiants donné.
+    # récupére les notes sur les années précédentes pour aider à la saisie des cursus adaptés (cursus)
+    # (sportifs, étalement sur plusieurs années, etc.).
+    data_evals: lal iste des évaluations du module.
+    """
+
+    def __init__(self, etudiant: Identite, modimpl):
+        self.etudiant = etudiant
+        self.data_evals = []
+        self.modimpl = modimpl
+        self.modimpl_id = modimpl.id
+        self.module_code = modimpl.module.code
+        self.titre = modimpl.module.titre
+        href = url_for(
+            "notes.moduleimpl_status",
+            scodoc_dept=g.scodoc_dept,
+            moduleimpl_id=modimpl.id,
+        )
+        self.link = (
+            f'<a class="stdlink" href="{href}">{self.module_code} {self.titre}</a>.'
+        )
+        # self.cursus = {}
+        formation_id = modimpl.formsemestre.formation_id
+        # for ins in etudiant.inscriptions():
+        #     if ins.formsemestre.formation_id == formation_id:
+        #         self.cursus[ins.formsemestre.id] = ins.formsemestre
+
+    def add_eval(self, data_eval):
+        self.data_evals.append(data_eval)
+
+
+class DataEval:
+    """Récupère les données sur une évaluation appliquée à un étudiant"""
+
+    def __init__(self, etudid, evaluation):
+        self.etudid = etudid
+        self.evaluation = evaluation
+        self.evaluation_id = evaluation.id
+        self.note_min, self.note_max = get_bounds(evaluation)
+        self.data_debut = evaluation.date_fin
+        self.date_fin = evaluation.date_fin
+        self.description = evaluation.description
+        self.ue_poids = evaluation.get_ue_poids_dict()
+        self.evaluation_type = evaluation.evaluation_type
+        self.note = NotesNotes.query.filter_by(
+            evaluation_id=self.evaluation.id, etudid=self.etudid
+        ).first()
+        self.note_value = "" if self.note is None else self.note.value
+        self.history = f"""<span id="formnotes_hist_{self.evaluation_id}">{
+        get_note_history_menu(evaluation.id, etudid)
+        }</span>"""
+
+    def forminput(self):
+        return f"""<input type="text" size="5" id="formnotes_note_{self.evaluation_id}" 
+        class="note" onkeypress="return enter_focus_next(this, event);" 
+        note_min="{self.note_min}" note_max="{self.note_max}" evaluation_id="{self.evaluation_id}"
+        original-value="{self.note_value}" value="{self.note_value}" />"""
+
+
+def get_data(semestre: FormSemestre, etudiant: Identite) -> dict:
+    """Récupère les infos pour un étudiant (cf ticket 1030)"""
+    res = res_sem.load_formsemestre_results(semestre)
+    data = {
+        "decision_jury": res.etud_has_decision(etudiant.etudid, include_rcues=False),
+        "data_sections": {},
+    }
+    if semestre.formation.is_apc():
+        for module_type in ModuleType:
+            data["data_sections"][module_type] = DataSection(module_type)
+    for modimpl in semestre.modimpls_sorted:
+        module_type = modimpl.module.module_type
+        if res.modimpl_inscr_df[modimpl.id][etudiant.id]:
+            data_modimpl = DataModimpl(etudiant, modimpl)
+            for evaluation in modimpl.evaluations:
+                data_modimpl.add_eval(DataEval(etudiant.etudid, evaluation))
+            data["data_sections"][module_type].add_modimpl(data_modimpl)
+    return data
+
+
+def saisie_notes_par_etu(semestre: FormSemestre, etudiant: Identite):
+    # Check access
+    # (admin, respformation, and responsable_id)
+    locked = not semestre.etat
+    # readonly peut être uinutile si l accès à cette page n est pas autorisée au public
+    readonly = locked or not (
+        current_user.has_permission(Permission.EditAllNotes)
+        or (current_user in semestre.responsables)
+    )
+    inscriptions = etudiant.formsemestre_inscriptions
+    inscription = None
+    for ins in inscriptions:
+        if ins.formsemestre_id == semestre.id:
+            inscription = ins
+    # assert ins is not None
+    if ins is None:
+        raise ValueError("Tentative d accès à une inscription inexistante")  # bug
+    if ins.etat != "I":
+        raise ValueError(
+            "Etudiant démissionnaire ou défaillant"
+        )  # ne devrait pas être accessible. bug
+    "Formulaire de saisie de note centré sur l'étudiant"
+    data = get_data(semestre, etudiant)
+    return render_template(
+        "etud/saisie_notes_par_etu.j2",
+        title="Saisie notes par étudiant",
+        javascripts=["js/saisie_notes.js", "js/saisie_notes_par_etu.js"],
+        sco=ScoData(formsemestre=semestre),
+        semestre=semestre,
+        etudiant=etudiant,
+        data=data,
+        readonly=readonly,
+        get_note_history_menu=get_note_history_menu,
+        module_type_order=[
+            ModuleType.RESSOURCE,
+            ModuleType.SAE,
+            ModuleType.STANDARD,
+            ModuleType.MALUS,
+        ],
+    )
+
+
+# US 1030 - FIN
+
+
+def get_evaluation_etud_note(evaluation: Evaluation, etudid: int):
+    a = Notes.query.filter_by(id=evaluation.id, etudid=etudid).first()
+    pass
+
+
 def get_sorted_etuds_notes(
     evaluation: Evaluation, etudids: list, formsemestre_id: int
 ) -> list[dict]:
@@ -815,13 +973,13 @@ def get_sorted_etuds_notes(
                 if notes_db[etudid]["uid"]
                 else None
             )
-            e["explanation"] = (
-                f"""{
-                notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M")
+            e[
+                "explanation"
+            ] = f"""{
+            notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M")
             } par {user.get_nomplogin() if user else '?'
             } {(' : ' + notes_db[etudid]["comment"]) if notes_db[etudid]["comment"] else ''}
             """
-            )
         else:
             e["val"] = ""
             e["explanation"] = ""
@@ -941,13 +1099,13 @@ def _form_saisie_notes(
             etud_classes.append("group-" + str(group_info["group_id"]))
 
         label = f"""<span class="{classdem}">{e["civilite_str"]} {
-            scu.format_nomprenom(e, reverse=True)}{observation}</span>"""
+        scu.format_nomprenom(e, reverse=True)}{observation}</span>"""
 
         # Historique des saisies de notes:
         explanation = (
             ""
             if disabled
-            else f"""<span id="hist_{etudid}">{
+            else f"""<span id="formnotes_hist_{etudid}">{
             get_note_history_menu(evaluation.id, etudid)
             }</span>"""
         )
@@ -1008,7 +1166,7 @@ def _form_saisie_notes(
     H.append(tf.getform())  # check and init
     H.append(
         f"""<a href="{url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
-        moduleimpl_id=modimpl.id)
+                              moduleimpl_id=modimpl.id)
         }" class="btn btn-primary link-terminer">Terminer</a>
         """
     )
@@ -1021,14 +1179,14 @@ def _form_saisie_notes(
         <div>
         <form id="do_evaluation_set_missing" action="{
             url_for("notes.do_evaluation_set_missing", scodoc_dept=g.scodoc_dept)
-        }" method="POST">
+            }" method="POST">
         Mettre les notes manquantes à
         <input type="text" size="5" name="value"/>
         <input type="submit" value="OK"/>
         <input type="hidden" name="evaluation_id" value="{evaluation.id}"/>
         <input class="group_ids_str" type="hidden" name="group_ids_str" value="{
             ",".join([str(x) for x in groups_infos.group_ids])
-        }"/>
+            }"/>
         <em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
         </form>
         </div>
@@ -1061,6 +1219,7 @@ def save_notes(
         return json_error(403, "modification notes non autorisee pour cet utilisateur")
     #
     valid_notes, _, _, _, _ = check_notes(notes, evaluation)
+
     if valid_notes:
         etudids_changed, _, etudids_with_decision, messages = notes_add(
             current_user, evaluation.id, valid_notes, comment=comment, do_it=True
@@ -1069,7 +1228,7 @@ def save_notes(
             typ=ScolarNews.NEWS_NOTE,
             obj=evaluation.moduleimpl_id,
             text=f"""Notes dans <a href="{status_url}">{
-                    evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
+            evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
             url=status_url,
             max_frequency=30 * 60,  # 30 minutes
         )
@@ -1120,7 +1279,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
             nv = ""  # ne repete pas la valeur de la note courante
         else:
             # ancienne valeur
-            nv = f": {dispnote}"
+            nv = f": {scu.fmt_note(dispnote)}"
         first = False
         if i["comment"]:
             comment = f' <span class="histcomment">{i["comment"]}</span>'
diff --git a/app/static/js/saisie_notes.js b/app/static/js/saisie_notes.js
index 4ed8e15760f2ea88da8d3865551c05133f71a2c2..553c30016f7c4322a93da1c394244003d411dd94 100644
--- a/app/static/js/saisie_notes.js
+++ b/app/static/js/saisie_notes.js
@@ -1,4 +1,44 @@
-// Formulaire saisie des notes
+// Procedures communes pour formulaires saisie des notes
+/***************************************************
+ Pré-requis pour l'utilisation des fonctionnalités communes à saisie_notes_par_etu et saisie_notes_par_eval
+ * il existe un champ d'id `formnotes_formsemestre_id` qui donne l'id du formsemestre courant
+ * Les champs de saisie de notes:
+     * répondent au sélecteur ("#formnotes .note")
+     * possèdent un id de la forme `formnotes_note_###` (### identifiant étudiant, évaluation ou autre)
+     * possèdent les attributs initialisés `data-last-saved-value`, `data-original-value` `data_modified` = false;
+ * Les champs historiques:
+     * possèdent un id de la forme `hist_###` (### identifiant étudiant, évaluation ou autre)
+ * les fonctions javascript suivantes sont implémentées:
+     * get_id(field): str (retourne le ### pour un champ note donné)
+     * get_evaluation_id(identifiant: str): int
+     * get formsemestre_id(identifiant: str): int
+     * get_note_min(identifiant: str): int
+     * get_note_max(identifiant: str): int
+     * update_jurylink(note_field: element, etudid: int)
+
+ Fonctionnement:
+     Un identifiant (field_id) represente un chaîne caractéristique d'une ligne de saisie.
+         * etudid pour le formulaire de saisie par évaluation
+         * evaluation_id pour le formulaire de saisie par étudiant
+     un attribut data-modified inque si un changement a eu lieu sur une zone de saisie.
+ initialement
+     l attribut data-modified indique toute modification
+     dans le traitement on_blur on vérifie si la valeur a été changée ou pas
+     exemple '15' changée en '15.0'
+        input => data-modifed = true
+        blur => rien n'est fait (la valeur n est pas changée)
+ modification (JMP)
+     exemple '15' changée en '15.0'
+        input => data-modified = false
+ *****************************************************/
+
+
+const note_prefix = "formnotes_note_";
+const hist_prefix = "formnotes_hist_";
+
+function get_formsemestre_id() {
+  return document.getElementById("formnotes_formsemestre_id").getAttribute("value");;
+}
 
 let nbSaving = 0; // nombre de requêtes en cours
 
@@ -24,52 +64,41 @@ function decSaving() {
   }
 }
 
-document.addEventListener("DOMContentLoaded", function () {
-  let noteInputs = document.querySelectorAll("#formnotes .note");
+window.addEventListener('beforeunload', function (e) {
+  const noteInputs = document.querySelectorAll("#formnotes .note");
   noteInputs.forEach(function (input) {
-    input.addEventListener("input", function() {
-      this.setAttribute("data-modified", "true");
-    });
-    input.addEventListener("blur", input.addEventListener("blur", function(event){write_on_blur(event.currentTarget)} ));
-  });
-
-  var formInputs = document.querySelectorAll("#formnotes input");
-  formInputs.forEach(function (input) {
-    input.addEventListener("paste", paste_text);
+    if (input.getAttribute("data-modified") === "true" && input.value !== input.getAttribute("data-last-saved-value")) {
+      valid_note.call(input);
+    }
   });
-
-  var masquerBtn = document.querySelector(".btn_masquer_DEM");
-  masquerBtn.addEventListener("click", masquer_DEM);
+  if (nbSaving > 0) {
+    // Display a confirmation dialog
+    const confirmationMessage = 'Des modifications sont en cours de sauvegarde. Êtes-vous sûr de vouloir quitter cette page ?';
+    e.preventDefault(); // Standard for most modern browsers
+    e.returnValue = confirmationMessage; // For compatibility with older browsers
+    return confirmationMessage; // For compatibility with older browsers
+  }
 });
 
-function write_on_blur(elt) {
-    if (elt.getAttribute("data-modified") === "true" && elt.value !== elt.getAttribute("data-last-saved-value")) {
-      valid_note.call(elt);
-      elt.setAttribute("data-modified", "false"); // Reset the modified flag
-    }
-}
-
-function is_valid_note(v) {
+function is_valid_note(field_id, v) {
   if (!v) return true;
 
-  var note_min = parseFloat(document.querySelector("#eval_note_min").textContent);
-  var note_max = parseFloat(document.querySelector("#eval_note_max").textContent);
-
   if (!v.match("^-?[0-9]*.?[0-9]*$")) {
     return v == "ABS" || v == "EXC" || v == "SUPR" || v == "ATT" || v == "DEM";
   } else {
-    var x = parseFloat(v);
-    return x >= note_min && x <= note_max;
+    const x = parseFloat(v);
+    return x >= get_note_min(field_id) && x <= get_note_max(field_id);
   }
 }
 
-function valid_note(e) {
-  var v = this.value.trim().toUpperCase().replace(",", ".");
-  if (is_valid_note(v)) {
+function valid_note() {
+  const field_id = get_field_id(this)
+  const v = this.value.trim().toUpperCase().replace(",", ".");
+  if (is_valid_note(field_id, v)) {
     if (v && v != this.getAttribute("data-last-saved-value")) {
       this.className = "note_valid_new";
-      const etudid = parseInt(this.getAttribute("data-etudid"));
-      save_note(this, v, etudid);
+      const etudid = get_etudid(field_id);
+      save_note(field_id, v);
     }
   } else {
     /* Saisie invalide */
@@ -78,10 +107,23 @@ function valid_note(e) {
   }
 }
 
-async function save_note(elem, v, etudid) {
-  let evaluation_id = document.querySelector("#formnotes_evaluation_id").getAttribute("value");
-  let formsemestre_id = document.querySelector("#formnotes_formsemestre_id").getAttribute("value");
-  var scoMsg = document.getElementById("sco_msg");
+function get_field_id(elt) {      // la forme des id est ????_#### (où #### est le  field_id que l'on cherche)
+  return elt.id.split('_').pop(); // formnotes_note_#### ou eval_####
+}
+
+function write_on_blur(elt) {
+    if (elt.getAttribute("data-modified") === "true") {
+      valid_note.call(elt);
+      elt.setAttribute("data-modified", "false"); // Reset the modified flag
+    }
+}
+
+async function save_note(field_id, v) {
+  const evaluation_id = get_evaluation_id(field_id);
+  const etudid = get_etudid(field_id);
+  const scoMsg = document.getElementById("sco_msg");
+  const note_field = get_note_field(field_id);
+  const hist_field = get_hist_field(field_id);
   scoMsg.innerHTML = "en cours...";
   scoMsg.style.display = "block";
   incSaving(); // update counter to show one more saving in progress
@@ -103,106 +145,51 @@ async function save_note(elem, v, etudid) {
       sco_message("Erreur: valeur non enregistrée");
     } else {
       const data = await response.json();
-      var scoMsg = document.getElementById("sco_msg");
       scoMsg.style.display = "none";
       if (data.etudids_changed.length > 0) {
         sco_message("enregistré");
-        elem.className = "note_saved";
+        note_field.className = "note_saved";
         // Il y avait une decision de jury ?
         if (data.etudids_with_decision.includes(etudid)) {
-          if (v !== elem.getAttribute("data-orig-value")) {
-            var juryLink = document.getElementById("jurylink_" + etudid);
-            juryLink.innerHTML = `<a href="${SCO_URL}Notes/formsemestre_validation_etud_form?formsemestre_id=${formsemestre_id}&etudid=${etudid}"
-              >mettre à jour décision de jury</a>`;
-          } else {
-            var juryLink = document.getElementById("jurylink_" + etudid);
-            juryLink.innerHTML = "";
-          }
+          update_jurylink(field_id, v);
         }
         // Mise à jour menu historique
         if (data.history_menu[etudid]) {
-          var historyElem = document.getElementById("hist_" + etudid);
-          historyElem.innerHTML = data.history_menu[etudid];
+          hist_field.innerHTML = data.history_menu[etudid];
         }
-        elem.setAttribute("data-last-saved-value", v);
+        note_field.setAttribute("data-last-saved-value", v);
       }
     }
   } catch (error) {
     console.error("Fetch error:", error);
     sco_message("Erreur réseau: valeur non enregistrée");
   } finally {
-    decSaving(); // update counter to show one saving in progress less. May re-enable 'Terminer' button
+    decSaving(); // mise à jour du nombre de requêtes en instance. si devient nul, réactive le lien 'Terminer'
   }
 }
 
-// Set up the beforeunload event listener
-window.addEventListener('beforeunload', function (e) {
-  let noteInputs = document.querySelectorAll("#formnotes .note");
-  noteInputs.forEach(function (input) {
-    if (input.getAttribute("data-modified") === "true" && input.value !== input.getAttribute("data-last-saved-value")) {
-      valid_note.call(input);
-    }
-  });
-  if (nbSaving > 0) {
-    // Display a confirmation dialog
-    const confirmationMessage = 'Des modifications sont en cours de sauvegarde. Êtes-vous sûr de vouloir quitter cette page ?';
-    e.preventDefault(); // Standard for most modern browsers
-    e.returnValue = confirmationMessage; // For compatibility with older browsers
-    return confirmationMessage; // For compatibility with older browsers
-  }
-});
-
-function change_history(e) {
-  let opt = e.selectedOptions[0];
-  let val = opt.getAttribute("data-note");
-  const etudid = parseInt(e.getAttribute("data-etudid"));
-  // le input associé a ce menu:
-  let input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
-  input_elem.value = val;
-  save_note(input_elem, val, etudid);
+function make_jurylink(field_id) {
+    const formsemestre_id = get_formsemestre_id();
+    const etudid = get_etudid(field_id);
+    const href = `${SCO_URL}Notes/formsemestre_validation_etud_form?formsemestre_id=${formsemestre_id}&etudid=${etudid}`;
+    return '<a href="' + href +'">mettre à jour décision de jury</a>';
 }
 
-// Contribution S.L.: copier/coller des notes
-
-function paste_text(event) {
-  event.stopPropagation();
-  event.preventDefault();
-  var clipb = event.clipboardData;
-  var data = clipb.getData("Text");
-  var list = data.split(/\r\n|\r|\n|\t| /g);
-  var currentInput = event.currentTarget;
-  var masquerDEM = document
-    .querySelector("body")
-    .classList.contains("masquer_DEM");
-
-  for (var i = 0; i < list.length; i++) {
-    if (!currentInput.disabled) { // skip DEM
-       currentInput.value = list[i];
-    }
-    // --- trigger blur
-    currentInput.setAttribute("data-modified", "true");
-    var evt = new Event("blur", { bubbles: true, cancelable: true});
-    currentInput.dispatchEvent(evt);
-    // --- next input
-    var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
-    while (
-      sibbling &&
-      (sibbling.style.display == "none" ||
-        (masquerDEM && sibbling.classList.contains("etud_dem")))
-    ) {
-      sibbling = sibbling.nextElementSibling;
-    }
-    if (sibbling) {
-      currentInput = sibbling.querySelector("input");
-      if (!currentInput) {
-        return;
+function change_history(e) {
+  const opt = e.selectedOptions[0];
+  const val = opt.getAttribute("data-note");
+  if (val != '') {
+      const field_id = get_field_id(e.parentElement);
+      // le input associé a ce menu:
+      const input = get_note_field(field_id)
+      save_note(field_id, val);
+      input.value = val
+      // gère le style de l'input (note_saved ou pas selon que la valeur de la note a changé par rapport à la valeur initiale)
+      if (val == input.getAttribute("data-orig-value")) {
+          input.classList.remove("note_saved")
+      } else {
+          input.classList.add("note_saved");
       }
-    } else {
-      return;
-    }
   }
 }
 
-function masquer_DEM() {
-  document.querySelector("body").classList.toggle("masquer_DEM");
-}
diff --git a/app/static/js/saisie_notes_par_etu.js b/app/static/js/saisie_notes_par_etu.js
new file mode 100644
index 0000000000000000000000000000000000000000..5c907152298ad5b625a02b99a8bd76395c6f5635
--- /dev/null
+++ b/app/static/js/saisie_notes_par_etu.js
@@ -0,0 +1,81 @@
+/*
+ * Gestion des formulaires de note
+ */
+
+// Formulaire saisie de notes pour une évaluation
+
+const form_etudid = parseInt(document.getElementById("etudiant_id").getAttribute("value"));
+
+function get_note_min(field_id) {
+  const note_field = get_note_field(field_id);
+  return parseFloat(note_field.getAttribute("note_min"));
+}
+
+function get_note_max(field_id) {
+  const note_field = get_note_field(field_id);
+  return parseFloat(note_field.getAttribute("note_max"));
+}
+
+function get_evaluation_id(field_id) {
+  const note_field = get_note_field(field_id);
+  return parseInt(note_field.getAttribute("evaluation_id"));
+}
+
+function get_note_field(field_id) {
+  return document.getElementById(note_prefix + field_id);
+}
+
+function get_hist_field(field_id) {
+  return document.getElementById(hist_prefix + field_id);
+}
+
+function get_etudid(field_id) {
+  return parseInt(form_etudid);
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+  const noteInputs = document.querySelectorAll("#formnotes .note");
+  noteInputs.forEach(function (input) {
+    input.addEventListener("input", function() {
+      if (this.value !== this.getAttribute("data-last-saved-value")) {
+        this.setAttribute("data-modified", "true");
+      }
+    });
+    input.addEventListener("blur", input.addEventListener("blur", function(event){
+      write_on_blur(event.currentTarget)
+    }));
+  });
+});
+
+function is_enter_key(event) {
+  if (event.keyCode == 13) return true;
+  if (event.which == 13) return true;
+  if (event.charCode == 13) return true;
+  return false;
+}
+
+function find_next_field(current_element) {
+  const form_input = Array.from(document.querySelectorAll("#formnotes input"))
+      .filter(elem => ! elem.disabled)
+      .filter(elem => $(elem).is(':visible'));
+  const current_index = form_input.indexOf(current_element)
+  return ((current_index == form_input.length -1) ? form_input[current_index] : form_input[current_index+1]);
+}
+function enter_focus_next (elem, event) {
+  if (is_enter_key(event)) {
+    let next_elem = find_next_field(elem);
+    if (next_elem != elem) {
+      next_elem.focus();
+      return true;
+    } else {
+      elem.blur();
+      return false;
+    }
+  }
+}
+
+function update_jurylink(evaluation_id, v) {
+  const note_field = get_note_field(evaluation_id);
+  const jurylink = document.getElementById('jurylink');
+    jurylink.innerHTML = make_jurylink(evaluation_id);
+}
\ No newline at end of file
diff --git a/app/static/js/saisie_notes_par_eval.js b/app/static/js/saisie_notes_par_eval.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4e226661f6df7e0aad961a0ad769bf0ee5ff567
--- /dev/null
+++ b/app/static/js/saisie_notes_par_eval.js
@@ -0,0 +1,105 @@
+// Formulaire saisie de notes pour une évaluation
+
+const note_min = parseFloat(document.querySelector("#eval_note_min").textContent);
+const note_max = parseFloat(document.querySelector("#eval_note_max").textContent);
+const form_evaluation_id = parseInt(document.getElementById("formnotes_evaluation_id").getAttribute("value"));
+
+function get_note_min(field_id) {
+  return note_min;
+}
+
+function get_note_max(field_id) {
+  return note_max;
+}
+
+function get_note_field(field_id) {
+  return document.getElementById(note_prefix + field_id);
+}
+
+function get_hist_field(field_id) {
+  return document.getElementById(hist_prefix + field_id);
+}
+
+function get_etudid(field_id) {
+  return parseInt(get_note_field(field_id).getAttribute("data-etudid"));
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+  const noteInputs = document.querySelectorAll("#formnotes .note");
+  noteInputs.forEach(function (input) {
+    console.log(get_field_id(input));
+    input.addEventListener("input", function() {
+      if (this.value !== this.getAttribute("data-last-saved-value")) {
+        this.setAttribute("data-modified", "true");
+      }
+    });
+    input.addEventListener("blur", input.addEventListener("blur", function(event){write_on_blur(event.currentTarget)} ));
+  });
+
+  const formInputs = document.querySelectorAll("#formnotes input");
+  formInputs.forEach(function (input) {
+    input.addEventListener("paste", paste_text);
+  });
+
+  const masquerBtn = document.querySelector(".btn_masquer_DEM");
+  masquerBtn.addEventListener("click", masquer_DEM);
+});
+
+function get_evaluation_id(field_id) {
+  return form_evaluation_id
+}
+
+// Contribution S.L.: copier/coller des notes
+
+function paste_text(event) {
+  event.stopPropagation();
+  event.preventDefault();
+  var clipb = event.clipboardData;
+  var data = clipb.getData("Text");
+  var list = data.split(/\r\n|\r|\n|\t| /g);
+  var currentInput = event.currentTarget;
+  var masquerDEM = document
+    .querySelector("body")
+    .classList.contains("masquer_DEM");
+
+  for (var i = 0; i < list.length; i++) {
+    if (!currentInput.disabled) { // skip DEM
+       currentInput.value = list[i];
+    }
+    // --- trigger blur
+    currentInput.setAttribute("data-modified", "true");
+    var evt = new Event("blur", { bubbles: true, cancelable: true});
+    currentInput.dispatchEvent(evt);
+    // --- next input
+    var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
+    while (
+      sibbling &&
+      (sibbling.style.display == "none" ||
+        (masquerDEM && sibbling.classList.contains("etud_dem")))
+    ) {
+      sibbling = sibbling.nextElementSibling;
+    }
+    if (sibbling) {
+      currentInput = sibbling.querySelector("input");
+      if (!currentInput) {
+        return;
+      }
+    } else {
+      return;
+    }
+  }
+}
+
+function masquer_DEM() {
+  document.querySelector("body").classList.toggle("masquer_DEM");
+}
+
+function update_jurylink(etudid, v) {
+  const juryLink = document.getElementById("jurylink_" + etudid);
+  const note_field = get_note_field(etudid)
+  if (v !== note_field.getAttribute("data-orig-value")) {
+    juryLink.innerHTML = make_jurylink(etudid);
+  } else {
+    juryLink.innerHTML = "";
+  }
+}
\ No newline at end of file
diff --git a/app/templates/etud/saisie_notes_par_etu.j2 b/app/templates/etud/saisie_notes_par_etu.j2
new file mode 100644
index 0000000000000000000000000000000000000000..b4f55ba97d8be2ea0b1a7dc8b7bfeae470e0f391
--- /dev/null
+++ b/app/templates/etud/saisie_notes_par_etu.j2
@@ -0,0 +1,109 @@
+{% extends 'sco_page.j2' %}
+
+{% block styles %}
+    {{super()}}
+    <link href="{{scu.STATIC_DIR}}/css/saisie_notes_par_etu.css" rel="stylesheet" type="text/css" />
+    <style>
+    #formnotes {
+        display:inline-grid;
+        grid-template-columns: 40px 40px minmax(350px, 1fr) 70px minmax(200px, 1fr);
+        align-items: baseline;
+    }
+    #formnotes .section {
+        grid-column: 1 / span 5;
+        border-bottom: solid black;
+        margin-bottom: 5px;
+        font-weight: bold;
+    }
+    #formnotes .module {
+        grid-column: 2 / span 4;
+        margin-bottom: 3px;
+        background-color: lightsteelblue;
+        padding-left: 5px;
+    }
+    #formnotes .label {
+        grid-column: 3;
+    }
+    #formnotes .input { grid-column: 4 }
+    #formnotes .history { grid-column: 5;}
+    #formnotes .long_input { grid-column: 4 / span 2 }
+    #formnotes .validation { grid-column: 2 / span 2 }
+    </style>
+{% endblock %}
+
+{% block app_content %}
+    <div id="saisie_notes_par_etu" class="table-container">
+        <span class="titre">{{title}}</span>
+        <h3> Saisie des notes de l'étudiant : {{etudiant.nom}} {{etudiant.prenom}} </h3>
+        {% if data["decision_jury"] %}
+        <div class="warning">
+            <ul class="tf-msg">
+            <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour
+            cet étudiant. Après changement des notes, vérifiez la situation !</li>
+            </ul>
+        </div>
+        {% endif %}
+        <div id="formnotes">
+            <input type="hidden" id="etudiant_id" value="{{etudiant.id}}" />
+            <input type="hidden" id="formnotes_formsemestre_id" value="{{semestre.id}}" />
+            {% if data["decision_jury"] %}
+            {% endif %}
+            <div class="label">commentaire</div>
+            <div class="long_input">
+                <input type="text" id="formnotes_comment" name="comment" class="input" size="44" onkeypress="return enter_focus_next(this, event);">
+            </div>
+            {% for module_type in module_type_order %}
+                {%  if data["data_sections"][module_type].data_modimpl %}
+                    <div class="section">{{ data["data_sections"][module_type].name }}</div>
+                    {% for data_modimpl in data["data_sections"][module_type].data_modimpl %}
+                       <div class="module">{{data_modimpl.link | safe}}</div>
+                       {% for data_eval in data_modimpl.data_evals %}
+                            <div class="label">{{ data_eval.description }}
+                                <input type="hidden" id="date_debut_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_debut }}">
+                                <input type="hidden" id="date_fin_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_fin }}">
+                                <input type="hidden" id="evaluation_type_{{ data_eval.evaluation_id }}" value="{{ data_eval.evaluation_type }}">
+                                    {% for ue in data_eval.ue_poids %}
+                                        <div id="poids_{{ data_eval.evaluation_id }}_{{ ue }}">
+                                            <input type="hidden" class="ue" value="{{ ue }}">
+                                            <input type="hidden" class="evaluation" value="{{ data_eval.evaluation_id }}">
+                                            <input type="hidden" value="{{ data_eval.ue_poids[ue] }}">
+                                        </div>
+                                    {% endfor %}
+                                </input>
+                            </div>
+                            <div class="input">
+                                {{ data_eval.forminput() | safe  }}
+                            </div>
+                            <div class="history">
+                                {{ data_eval.history | safe }}
+                            </div>
+                       {%  endfor %}
+                    {%  endfor %}
+                {% endif %}
+            {% endfor %}
+        <div class="validation">
+            <a href="/ScoDoc/INFO/Scolarite/Notes/moduleimpl_status?moduleimpl_id=21574"
+            class="btn btn-primary link-terminer"
+            style="pointer-events: unset;
+       color: unset; text-decoration: unset;
+       cursor: unset;">Terminer</a>
+            {% if data["decision_jury"] %}
+            <span id="jurylink" class="jurylink"></span>
+            {% endif %}
+        </div>
+    </div>
+        </form>
+    </div>
+    <div class="sco_help">
+        <p> 
+            Les modifications sont enregistrées au fur et à mesure.
+        </p>
+        <h4> Codes spéciaux: </h4>
+        <ul>
+            <li> ABS: absent (compte comme un zéro) </li>
+            <li> EXC: excusé (note neutralisée) </li>
+            <li> SUPR: pour supprimer une note existante </li>
+            <li> ATT: note en attente (permet de publier une évaluation avec des notes manquantes) </li>
+        </ul>
+    </div>
+{% endblock %}
diff --git a/app/views/notes.py b/app/views/notes.py
index 9a223f5059d5291e09f6f55a2db23a50e6c27b99..7dfa03bd23ff942b820e08bf4a21d4231460cf6d 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1816,6 +1816,22 @@ def form_saisie_notes(evaluation_id: int):
     return sco_saisie_notes.saisie_notes(evaluation, group_ids)
 
 
+# modification US 1030 - DEB
+
+
+@bp.route("/form_saisie_notes_par_etu/<int:semestre_id>/<int:etu_id>")
+@scodoc
+@permission_required(Permission.EnsView)
+def form_saisie_notes_par_etu(etu_id: int, semestre_id: int):
+    "Formulaire de saisie des notes centré sur l'étudiant"
+    etudiant = Identite.get_etud(etu_id)
+    semestre = FormSemestre.get_formsemestre(semestre_id)
+    return sco_saisie_notes.saisie_notes_par_etu(semestre, etudiant)
+
+
+# modification US 1030 - FIN
+
+
 @bp.route("/formsemestre_import_notes/<int:formsemestre_id>", methods=["GET", "POST"])
 @scodoc
 @permission_required(Permission.ScoView)  # controle contextuel