diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 988f041d7fa2796a08617bd5b58dc81a1e534204..ef321bb977a0aa900bbe4f749e4f20e13d5dcb34 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -236,11 +236,16 @@ class FormSemestre(models.ScoDocModel):
     def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
         """Création d'un formsemestre, avec toutes les valeurs par défaut
         et notification (sauf si silent).
+        Arguments:
+        - responsables peut être une liste de User ou de user_id.
         Crée la partition par défaut.
+        Commit all changes.
         """
         # was sco_formsemestre.do_formsemestre_create
         if "dept_id" not in args:
             args["dept_id"] = g.scodoc_dept_id
+        if "formation_id" not in args:
+            raise ScoValueError("create_formsemestre: no formation_id")
         formsemestre: "FormSemestre" = cls.create_from_dict(args)
         db.session.flush()
         for etape in args.get("etapes") or []:
@@ -1475,8 +1480,9 @@ notes_formsemestre_responsables = db.Table(
         "formsemestre_id",
         db.Integer,
         db.ForeignKey("notes_formsemestre.id"),
+        primary_key=True,
     ),
-    db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")),
+    db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
 )
 
 
diff --git a/migrations/versions/e0824c4f1b0b_responsables_assoc_unicity.py b/migrations/versions/e0824c4f1b0b_responsables_assoc_unicity.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f86328d0f8fc575702d9dfb4d201c85f94d9684
--- /dev/null
+++ b/migrations/versions/e0824c4f1b0b_responsables_assoc_unicity.py
@@ -0,0 +1,66 @@
+"""responsables_assoc_unicity
+
+Revision ID: e0824c4f1b0b
+Revises: bc85a55e63e1
+Create Date: 2025-02-03 16:45:13.082716
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "e0824c4f1b0b"
+down_revision = "bc85a55e63e1"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # Suppression des doublons dans la table d'association notes_formsemestre_responsables
+    op.execute(
+        """
+        WITH duplicates AS (
+            SELECT
+                formsemestre_id,
+                responsable_id,
+                ctid AS row_identifier,
+                row_number() OVER (
+                    PARTITION BY formsemestre_id, responsable_id
+                    ORDER BY ctid
+                ) AS rn
+            FROM notes_formsemestre_responsables
+        )
+        DELETE FROM notes_formsemestre_responsables
+        WHERE ctid IN (
+            SELECT row_identifier FROM duplicates WHERE rn > 1
+        );
+    """
+    )
+
+    with op.batch_alter_table(
+        "notes_formsemestre_responsables", schema=None
+    ) as batch_op:
+        batch_op.alter_column(
+            "formsemestre_id", existing_type=sa.INTEGER(), nullable=False
+        )
+        batch_op.alter_column(
+            "responsable_id", existing_type=sa.INTEGER(), nullable=False
+        )
+        batch_op.create_unique_constraint(
+            "uq_notes_formsemestre_responsables", ["formsemestre_id", "responsable_id"]
+        )
+
+
+def downgrade():
+    with op.batch_alter_table(
+        "notes_formsemestre_responsables", schema=None
+    ) as batch_op:
+        batch_op.alter_column(
+            "responsable_id", existing_type=sa.INTEGER(), nullable=True
+        )
+        batch_op.alter_column(
+            "formsemestre_id", existing_type=sa.INTEGER(), nullable=True
+        )
+        batch_op.drop_constraint("uq_notes_formsemestre_responsables", type_="unique")
diff --git a/sco_version.py b/sco_version.py
index 02a1ea25164ffb76100be08996e8efdfeeefefe3..eb6d6e298f388ee1e51db7700d458d3e128d866b 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -3,7 +3,7 @@
 
 "Infos sur version ScoDoc"
 
-SCOVERSION = "9.7.59"
+SCOVERSION = "9.7.60"
 
 SCONAME = "ScoDoc"
 
diff --git a/tests/unit/test_formsemestre.py b/tests/unit/test_formsemestre.py
index 9d0313624323dc79ab91f2741b21bee6e230178a..45d799bfee4e410c4fd28a5e6b64a7b1131bf86c 100644
--- a/tests/unit/test_formsemestre.py
+++ b/tests/unit/test_formsemestre.py
@@ -8,6 +8,7 @@ import pytest
 
 import app
 from app import db
+from app.auth.models import User
 from app.formations import edit_ue, formation_versions
 from app.models import Formation, FormSemestre, FormSemestreDescription
 from app.scodoc import (
@@ -207,6 +208,44 @@ def test_formsemestre_misc_views(test_client):
     # pas de test des indicateurs de suivi BUT
 
 
+def test_formsemestre_edit(test_client):
+    """Test modification formsemestre"""
+    u1 = User.query.first()
+    u2 = User(user_name="un_autre_responsable")
+    db.session.add(u2)
+    formation = Formation(
+        dept_id=1,
+        acronyme="FORM",
+        titre="Formation",
+        titre_officiel="La formation test",
+    )
+    db.session.add(formation)
+    db.session.flush()
+    formsemestre = FormSemestre.create_formsemestre(
+        {
+            "dept_id": 1,
+            "titre": "test edit",
+            "date_debut": "2024-08-01",
+            "date_fin": "2024-08-31",
+            "formation_id": formation.id,
+            "responsables": [u1, -1, -1, -1],
+        },
+        silent=True,
+    )
+    assert formsemestre
+    assert formsemestre.responsables[0].user_name == u1.user_name
+    assert formsemestre.from_dict({"responsables": [u2.id, u1, -1, -1]})
+    db.session.commit()
+    assert len(formsemestre.responsables) == 2
+    assert sorted(u.user_name for u in formsemestre.responsables) == sorted(
+        [u1.user_name, u2.user_name]
+    )
+    u3 = User(user_name="un_troisieme_responsable")
+    assert formsemestre.from_dict({"responsables": [u3]})
+    db.session.commit()
+    assert len(formsemestre.responsables) == 1
+
+
 def test_formsemestre_description(test_client):
     """Test FormSemestreDescription"""
     app.set_sco_dept(DEPT)