From 9ae2181904e772370d6bdf8e7fbeef4612b8c6b1 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Sun, 15 Sep 2024 16:50:25 +0200
Subject: [PATCH] =?UTF-8?q?CAS:=20ajout=20option=20pour=20utiliser=20par?=
 =?UTF-8?q?=20d=C3=A9faut=20le=20m=C3=AAme=20uid=20ScoDoc=20et=20CAS=20+?=
 =?UTF-8?q?=20cosmetic=20formulaires?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/auth/models.py                            |  15 +-
 app/forms/main/config_cas.py                  |   9 +
 app/models/config.py                          |   9 +-
 app/static/css/scodoc.css                     |   8 +-
 app/static/css/scodoc97.css                   |  12 +-
 app/templates/auth/cas_users_import_config.j2 |  29 +-
 app/templates/config_cas.j2                   |  43 +-
 app/templates/wtf.j2                          |  11 +-
 app/views/scodoc.py                           |   8 +-
 app/views/users.py                            | 433 +++++++++---------
 10 files changed, 327 insertions(+), 250 deletions(-)

diff --git a/app/auth/models.py b/app/auth/models.py
index 9e0049ac7..440117ed9 100644
--- a/app/auth/models.py
+++ b/app/auth/models.py
@@ -355,12 +355,15 @@ class User(UserMixin, ScoDocModel):
 
         super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
 
-        # Set cas_id using regexp if configured:
-        exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
-        if exp and self.email_institutionnel:
-            cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
-            if cas_id:
-                self.cas_id = cas_id
+        if ScoDocSiteConfig.cas_uid_use_scodoc():
+            self.cas_id = self.user_name
+        else:
+            # Set cas_id using regexp if configured:
+            exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
+            if exp and self.email_institutionnel:
+                cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
+                if cas_id:
+                    self.cas_id = cas_id
 
     def get_token(self, expires_in=3600):
         "Un jeton pour cet user. Stocké en base, non commité."
diff --git a/app/forms/main/config_cas.py b/app/forms/main/config_cas.py
index 41c639bcf..5b1a18fa5 100644
--- a/app/forms/main/config_cas.py
+++ b/app/forms/main/config_cas.py
@@ -98,6 +98,15 @@ class ConfigCASForm(FlaskForm):
         validators=[Optional(), check_cas_uid_from_mail_regexp],
     )
 
+    cas_uid_use_scodoc = BooleanField(
+        "Utiliser l'identifiant ScoDoc comme identifiant CAS",
+        description="""Si coché, l'identifiant ScoDoc sera utilisé comme identifiant CAS,
+        sans transformation. Cette option est utile si les identifiants ScoDoc sont déjà
+        des identifiants CAS.
+        Dans ce cas, l'adresse mail (réglage ci-dessus) n'est pas utilisée.
+        """,
+    )
+
     cas_edt_id_from_xml_regexp = StringField(
         label="Optionnel: expression pour extraire l'identifiant edt",
         description="""regexp python appliquée à la réponse XML du serveur CAS pour
diff --git a/app/models/config.py b/app/models/config.py
index f5e23bfce..25d9b9ccb 100644
--- a/app/models/config.py
+++ b/app/models/config.py
@@ -105,6 +105,7 @@ class ScoDocSiteConfig(db.Model):
         "cas_validate_route": str,
         "cas_attribute_id": str,
         "cas_uid_from_mail_regexp": str,
+        "cas_uid_use_scodoc": bool,
         "cas_edt_id_from_xml_regexp": str,
         # Assiduité
         "morning_time": str,
@@ -239,6 +240,12 @@ class ScoDocSiteConfig(db.Model):
         cfg = ScoDocSiteConfig.query.filter_by(name="cas_force").first()
         return cfg is not None and cfg.value
 
+    @classmethod
+    def cas_uid_use_scodoc(cls) -> bool:
+        """True si cas_uid_use_scodoc"""
+        cfg = ScoDocSiteConfig.query.filter_by(name="cas_uid_use_scodoc").first()
+        return cfg is not None and cfg.value
+
     @classmethod
     def is_entreprises_enabled(cls) -> bool:
         """True si on doit activer le module entreprise"""
@@ -404,7 +411,7 @@ class ScoDocSiteConfig(db.Model):
 
     @classmethod
     def extract_cas_id(cls, email_addr: str) -> str | None:
-        "Extract cas_id from maill, using regexp in config. None if not possible."
+        "Extract cas_id from mail, using regexp in config. None if not possible."
         exp = cls.get("cas_uid_from_mail_regexp")
         if not exp or not email_addr:
             return None
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 56e76e299..a54cb42c2 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -1246,7 +1246,8 @@ a.discretelink:hover {
   text-align: center;
 }
 
-.help {
+.help,
+.help-block {
   max-width: var(--sco-content-max-width);
   font-style: italic;
 }
@@ -1260,6 +1261,10 @@ a.discretelink:hover {
   color: red;
 }
 
+div.help-block {
+  margin-bottom: 16px;
+}
+
 div.sco_box,
 div.sco_help {
   margin-top: 12px;
@@ -4919,7 +4924,6 @@ table.formation_table_recap td.heures_tp {
 }
 
 div.cas_settings {
-  margin-left: -15px;
   margin-bottom: 8px;
   border: 1px dashed rgb(191, 34, 191);
   background-color: #feb4e54f;
diff --git a/app/static/css/scodoc97.css b/app/static/css/scodoc97.css
index ed06fb6d3..d1300794b 100644
--- a/app/static/css/scodoc97.css
+++ b/app/static/css/scodoc97.css
@@ -430,11 +430,21 @@ textarea {
     color: #333;
 }
 
+form.form div.checkbox {
+    margin-top: 6px;
+    margin-bottom: 6px;
+}
+
+div.form-group {
+    margin-top: 16px;
+    margin-bottom: 6px;
+}
+
 .form-group input,
 .form-control {
     width: 100%;
     padding: 10px;
-    margin-bottom: 16px;
+    margin-bottom: 4px;
     border: 1px solid #ced4da;
     border-radius: 4px;
     font-size: 16px;
diff --git a/app/templates/auth/cas_users_import_config.j2 b/app/templates/auth/cas_users_import_config.j2
index 73fb34a65..bccc4ace3 100644
--- a/app/templates/auth/cas_users_import_config.j2
+++ b/app/templates/auth/cas_users_import_config.j2
@@ -4,7 +4,7 @@
 {% block app_content %}
 <h1>Chargement des configurations CAS des utilisateurs</h1>
 
-<div style="max-width: 800px;">
+<div class="scobox help explanation">
 <p style="color: red">A utiliser pour modifier le paramétrage CAS de
     <b>comptes utilisateurs existants</b>
 </p>
@@ -32,21 +32,26 @@
     <li style="margin-bottom:32px;">Revenez sur cette page et chargez le fichier dans ScoDoc.
     </li>
 </ol>
+</div>
+
+<div class="scobox">
+    <div class="scobox-title">Étape 1: exporter fichier Excel à charger</div>
+    <ul>
+        <li><a class="stdlink" href="{{
+            url_for('auth.cas_users_generate_excel_sample')
+        }}">Obtenir la feuille excel à remplir</a>,
+        avec la liste complète des utilisateurs.
+        </li>
+    </ul>
+</div>
+
+<div class="scobox">
+    <div class="scobox-title">Étape 2: charger le fichier Excel modifié</div>
 
-<ul>
-    <li><b>Étape 1: </b><a class="stdlink" href="{{
-        url_for('auth.cas_users_generate_excel_sample')
-    }}">Obtenir la feuille excel à remplir</a>,
-    avec la liste complète des utilisateurs.
-    </li>
-    <li style="margin-top: 16px;"><b>Étape 2:</b>
     <div class="row">
         <div class="col-md-8">
             {{ wtf.quick_form(form) }}
         </div>
     </div>
-</li>
-</ul>
-
-
+</div>
 {% endblock %}
diff --git a/app/templates/config_cas.j2 b/app/templates/config_cas.j2
index b1d532f88..a3bca2e4d 100644
--- a/app/templates/config_cas.j2
+++ b/app/templates/config_cas.j2
@@ -1,6 +1,14 @@
 {% extends "base.j2" %}
 {% import 'wtf.j2' as wtf %}
 
+
+{% block styles %}
+{{super()}}
+<style>
+    div.checkbox label { font-weight: normal; }
+</style>
+{% endblock %}
+
 {% block app_content %}
 <h1>Configuration du Service d'Authentification Central (CAS)</h1>
 
@@ -18,14 +26,33 @@
         {{ wtf.form_field(form.cas_enable) }}
         {{ wtf.form_field(form.cas_force) }}
         {{ wtf.form_field(form.cas_allow_for_new_users) }}
-        {{ wtf.form_field(form.cas_server) }}
-        {{ wtf.form_field(form.cas_login_route) }}
-        {{ wtf.form_field(form.cas_logout_route) }}
-        {{ wtf.form_field(form.cas_validate_route) }}
-        {{ wtf.form_field(form.cas_attribute_id) }}
-        {{ wtf.form_field(form.cas_uid_from_mail_regexp) }}
-        {{ wtf.form_field(form.cas_edt_id_from_xml_regexp) }}
-        <div class="cas_settings">
+        <div class="scobox">
+            <div class="scobox-title">Routes CAS</div>
+            {{ wtf.form_field(form.cas_server) }}
+            {{ wtf.form_field(form.cas_login_route) }}
+            {{ wtf.form_field(form.cas_logout_route) }}
+            {{ wtf.form_field(form.cas_validate_route) }}
+        </div>
+        <div class="scobox">
+            {{ wtf.form_field(form.cas_attribute_id) }}
+        </div>
+        <div class="scobox">
+            <div class="scobox-title">Identifiant utilisateur CAS</div>
+            <div class="help explanation">
+                Ces paramètres sont utilisés pour déduire
+                l'identifiant CAS des utilisateurs ScoDoc au moment de la création ou modification
+                de comptes utilisateurs. Pour modifier les comptes existants en masse, il peut être
+                pratique de passer par un
+                <a class="stdlink" href="{{ url_for('auth.cas_users_import_config') }}">export/import excel</a>.
+            </div>
+            {{ wtf.form_field(form.cas_uid_from_mail_regexp) }}
+            {{ wtf.form_field(form.cas_uid_use_scodoc) }}
+        </div>
+        <div class="scobox">
+            {{ wtf.form_field(form.cas_edt_id_from_xml_regexp) }}
+        </div>
+        <div class="scobox cas_settings">
+            <div class="scobox-title">Certificat serveur CAS</div>
             {{ wtf.form_field(form.cas_ssl_verify) }}
             {{ wtf.form_field(form.cas_ssl_certificate_file) }}
             <div class="cas_etat_certif_ssl">Certificat SSL
diff --git a/app/templates/wtf.j2 b/app/templates/wtf.j2
index 7f69ca9ad..cefb2f18b 100644
--- a/app/templates/wtf.j2
+++ b/app/templates/wtf.j2
@@ -43,6 +43,9 @@ the necessary fix for required=False attributes, but will also not set the requi
     <div class="checkbox">
       <label>
         {{field()|safe}} {{field.label.text|safe}}
+        {%- if field.description %}
+          <div class="help-block">{{field.description|safe}}</div>
+        {%- endif %}
       </label>
     </div>
   {% endcall %}
@@ -108,12 +111,12 @@ the necessary fix for required=False attributes, but will also not set the requi
         {%- if field.errors %}
           {%- for error in field.errors %}
             {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
-              <p class="help-block">{{error}}</p>
+              <div class="help-block">{{error}}</div>
             {% endcall %}
           {%- endfor %}
         {%- elif field.description -%}
           {% call _hz_form_wrap(horizontal_columns, form_type, required=required) %}
-            <p class="help-block">{{field.description|safe}}</p>
+            <div class="help-block">{{field.description|safe}}</div>
           {% endcall %}
         {%- endif %}
       {%- else -%}
@@ -126,10 +129,10 @@ the necessary fix for required=False attributes, but will also not set the requi
 
         {%- if field.errors %}
           {%- for error in field.errors %}
-            <p class="help-block">{{error}}</p>
+            <div class="help-block">{{error}}</div>
           {%- endfor %}
         {%- elif field.description -%}
-          <p class="help-block">{{field.description|safe}}</p>
+          <div class="help-block">{{field.description|safe}}</div>
         {%- endif %}
       {%- endif %}
   </div>
diff --git a/app/views/scodoc.py b/app/views/scodoc.py
index 8096e927a..fcdf857f9 100644
--- a/app/views/scodoc.py
+++ b/app/views/scodoc.py
@@ -296,6 +296,11 @@ def config_cas():
             "cas_uid_from_mail_regexp", form.data["cas_uid_from_mail_regexp"]
         ):
             flash("Expression extraction identifiant CAS enregistrée")
+        if ScoDocSiteConfig.set("cas_uid_use_scodoc", form.data["cas_uid_use_scodoc"]):
+            if form.data["cas_uid_use_scodoc"]:
+                flash("Utilisation de l'identifiant ScoDoc comme identifiant CAS")
+            else:
+                flash("N'utilise PAS l'identifiant ScoDoc pour le CAS")
         if ScoDocSiteConfig.set(
             "cas_edt_id_from_xml_regexp", form.data["cas_edt_id_from_xml_regexp"]
         ):
@@ -313,7 +318,7 @@ def config_cas():
         set_cas_configuration()
         return redirect(url_for("scodoc.configuration"))
 
-    elif request.method == "GET":
+    if request.method == "GET":
         form.cas_enable.data = ScoDocSiteConfig.get("cas_enable")
         form.cas_force.data = ScoDocSiteConfig.get("cas_force")
         form.cas_allow_for_new_users.data = ScoDocSiteConfig.get(
@@ -327,6 +332,7 @@ def config_cas():
         form.cas_uid_from_mail_regexp.data = ScoDocSiteConfig.get(
             "cas_uid_from_mail_regexp"
         )
+        form.cas_uid_use_scodoc.data = ScoDocSiteConfig.get("cas_uid_use_scodoc")
         form.cas_edt_id_from_xml_regexp.data = ScoDocSiteConfig.get(
             "cas_edt_id_from_xml_regexp"
         )
diff --git a/app/views/users.py b/app/views/users.py
index dbd15b05c..3c147a6d6 100644
--- a/app/views/users.py
+++ b/app/views/users.py
@@ -404,9 +404,13 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
                 "input_type": "text",
                 "explanation": "id du compte utilisateur sur le CAS de l'établissement "
                 + (
-                    "(<b>sera déduit de son e-mail institutionnel</b>) "
-                    if ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
-                    else ""
+                    "<b>pa défaut identique à l'identifiant ScoDoc</b> "
+                    if ScoDocSiteConfig.get("cas_uid_use_scodoc")
+                    else (
+                        "(<b>sera déduit de son e-mail institutionnel</b>) "
+                        if ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
+                        else ""
+                    )
                 )
                 + (
                     "(service CAS activé)"
@@ -537,7 +541,8 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
                     "d",
                     {
                         "input_type": "separator",
-                        "title": f"L'utilisateur  sera créé dans le département {auth_dept or 'aucun'}",
+                        "title": f"""L'utilisateur  sera créé dans le département {
+                            auth_dept or 'aucun'}""",
                     },
                 )
             )
@@ -606,240 +611,238 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
             content="\n".join(H) + "\n" + tf[1],
             javascripts=["js/user_form.js"],
         )
-    elif tf[0] == -1:
+    if tf[0] == -1:
         return flask.redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
+
+    vals = tf[2]
+    roles = set(vals["roles"]).intersection(editable_roles_strings)
+    if not current_user.is_administrator():
+        # empeche modification des paramètres CAS
+        if "cas_allow_login" in vals:
+            vals["cas_allow_login"] = cas_allow_login_default
+        if "cas_allow_scodoc_login" in vals:
+            if the_user is None:
+                vals.pop("cas_allow_scodoc_login", None)
+            else:
+                vals["cas_allow_scodoc_login"] = the_user.cas_allow_scodoc_login
+
+    if not current_user.has_permission(Permission.UsersChangeCASId):
+        vals.pop("cas_id", None)
+    if "edit" in vals:
+        edit = int(vals["edit"])
     else:
-        vals = tf[2]
-        roles = set(vals["roles"]).intersection(editable_roles_strings)
-        if not current_user.is_administrator():
-            # empeche modification des paramètres CAS
-            if "cas_allow_login" in vals:
-                vals["cas_allow_login"] = cas_allow_login_default
-            if "cas_allow_scodoc_login" in vals:
-                if the_user is None:
-                    vals.pop("cas_allow_scodoc_login", None)
-                else:
-                    vals["cas_allow_scodoc_login"] = the_user.cas_allow_scodoc_login
+        edit = 0
+    try:
+        force = int(vals.get("force", "0")[0])
+    except (IndexError, ValueError, TypeError):
+        force = 0
 
-        if not current_user.has_permission(Permission.UsersChangeCASId):
-            vals.pop("cas_id", None)
-        if "edit" in vals:
-            edit = int(vals["edit"])
-        else:
-            edit = 0
-        try:
-            force = int(vals.get("force", "0")[0])
-        except (IndexError, ValueError, TypeError):
-            force = 0
+    if edit:
+        user_name = initvalues["user_name"]
+    else:
+        user_name = vals["user_name"]
+    # ce login existe ?
+    err_msg = None
+    nb_existing_user = User.query.filter_by(user_name=user_name).count() > 0
+    if edit and (
+        nb_existing_user == 0
+    ):  # safety net, le user_name ne devrait pas changer
+        err_msg = f"identifiant {user_name} inexistant"
+    if not edit and nb_existing_user > 0:
+        err_msg = f"identifiant {user_name} déjà utilisé"
+    if err_msg:
+        H.append(tf_error_message(f"""Erreur: {err_msg}"""))
+        return render_template(
+            "base.j2",
+            content="\n".join(H) + "\n" + tf[1],
+            javascripts=["js/user_form.js"],
+        )
 
-        if edit:
-            user_name = initvalues["user_name"]
-        else:
-            user_name = vals["user_name"]
-        # ce login existe ?
-        err_msg = None
-        nb_existing_user = User.query.filter_by(user_name=user_name).count() > 0
-        if edit and (
-            nb_existing_user == 0
-        ):  # safety net, le user_name ne devrait pas changer
-            err_msg = f"identifiant {user_name} inexistant"
-        if not edit and nb_existing_user > 0:
-            err_msg = f"identifiant {user_name} déjà utilisé"
-        if err_msg:
-            H.append(tf_error_message(f"""Erreur: {err_msg}"""))
+    if not edit_only_roles:
+        ok_modif, msg = sco_users.check_modif_user(
+            edit,
+            enforce_optionals=not force,
+            user_name=user_name,
+            nom=vals["nom"],
+            prenom=vals["prenom"],
+            email=vals["email"],
+            dept=vals.get("dept", auth_dept),
+            roles=vals["roles"],
+            cas_id=vals.get("cas_id"),  # pas présent si pas super-admin
+        )
+        if not ok_modif:
+            H.append(tf_error_message(msg))
             return render_template(
                 "base.j2",
                 content="\n".join(H) + "\n" + tf[1],
                 javascripts=["js/user_form.js"],
             )
-
-        if not edit_only_roles:
-            ok_modif, msg = sco_users.check_modif_user(
-                edit,
-                enforce_optionals=not force,
-                user_name=user_name,
-                nom=vals["nom"],
-                prenom=vals["prenom"],
-                email=vals["email"],
-                dept=vals.get("dept", auth_dept),
-                roles=vals["roles"],
-                cas_id=vals.get("cas_id"),  # pas présent si pas super-admin
-            )
-            if not ok_modif:
-                H.append(tf_error_message(msg))
+        if "date_expiration" in vals:
+            try:
+                if vals["date_expiration"]:
+                    vals["date_expiration"] = datetime.datetime.strptime(
+                        vals["date_expiration"], scu.DATE_FMT
+                    )
+                    if vals["date_expiration"] < datetime.datetime.now():
+                        H.append(tf_error_message("date expiration passée"))
+                        return render_template(
+                            "base.j2",
+                            content="\n".join(H) + "\n" + tf[1],
+                            javascripts=["js/user_form.js"],
+                        )
+                else:
+                    vals["date_expiration"] = None
+            except ValueError:
+                H.append(tf_error_message("date expiration invalide"))
                 return render_template(
                     "base.j2",
                     content="\n".join(H) + "\n" + tf[1],
                     javascripts=["js/user_form.js"],
                 )
-            if "date_expiration" in vals:
-                try:
-                    if vals["date_expiration"]:
-                        vals["date_expiration"] = datetime.datetime.strptime(
-                            vals["date_expiration"], scu.DATE_FMT
-                        )
-                        if vals["date_expiration"] < datetime.datetime.now():
-                            H.append(tf_error_message("date expiration passée"))
-                            return render_template(
-                                "base.j2",
-                                content="\n".join(H) + "\n" + tf[1],
-                                javascripts=["js/user_form.js"],
-                            )
-                    else:
-                        vals["date_expiration"] = None
-                except ValueError:
-                    H.append(tf_error_message("date expiration invalide"))
-                    return render_template(
-                        "base.j2",
-                        content="\n".join(H) + "\n" + tf[1],
-                        javascripts=["js/user_form.js"],
-                    )
 
-        if edit:  # modif utilisateur (mais pas password ni user_name !)
-            if (not can_choose_dept) and "dept" in vals:
-                del vals["dept"]
-            if "password" in vals:
-                del vals["passwordd"]
-            if "date_modif_passwd" in vals:
-                del vals["date_modif_passwd"]
-            if "user_name" in vals:
-                del vals["user_name"]
-            if (current_user.user_name == user_name) and "status" in vals:
-                del vals["status"]  # no one can't change its own status
-            if "status" in vals:
-                vals["active"] = vals["status"] == ""
-            # Département:
-            if ("dept" in vals) and (vals["dept"] not in selectable_dept_acronyms):
-                del vals["dept"]  # ne change pas de dept
-            # Traitement des roles: ne doit pas affecter les rôles
-            # que l'on en contrôle pas:
-            for role in orig_roles_strings:  # { "Ens_RT", "Secr_CJ", ... }
-                if role and not role in editable_roles_strings:
-                    roles.add(role)
-
-            vals["roles_string"] = ",".join(roles)
-
-            # ok, edit
-            if not edit_only_roles:
-                log(f"sco_users: editing {user_name} by {current_user.user_name}")
-                log(f"sco_users: previous_values={initvalues}")
-                log(f"sco_users: new_values={vals}")
-            else:
-                vals = {"roles_string": vals["roles_string"]}
-            the_user.from_dict(vals)
-            db.session.add(the_user)
-            db.session.commit()
-            flash(f"Utilisateur {user_name} modifié")
-            return flask.redirect(
-                url_for(
-                    "users.user_info_page",
-                    scodoc_dept=g.scodoc_dept,
-                    user_name=user_name,
-                )
+    if edit:  # modif utilisateur (mais pas password ni user_name !)
+        if (not can_choose_dept) and "dept" in vals:
+            del vals["dept"]
+        if "password" in vals:
+            del vals["passwordd"]
+        if "date_modif_passwd" in vals:
+            del vals["date_modif_passwd"]
+        if "user_name" in vals:
+            del vals["user_name"]
+        if (current_user.user_name == user_name) and "status" in vals:
+            del vals["status"]  # no one can't change its own status
+        if "status" in vals:
+            vals["active"] = vals["status"] == ""
+        # Département:
+        if ("dept" in vals) and (vals["dept"] not in selectable_dept_acronyms):
+            del vals["dept"]  # ne change pas de dept
+        # Traitement des roles: ne doit pas affecter les rôles
+        # que l'on en contrôle pas:
+        for role in orig_roles_strings:  # { "Ens_RT", "Secr_CJ", ... }
+            if role and not role in editable_roles_strings:
+                roles.add(role)
+
+        vals["roles_string"] = ",".join(roles)
+
+        # ok, edit
+        if not edit_only_roles:
+            log(f"sco_users: editing {user_name} by {current_user.user_name}")
+            log(f"sco_users: previous_values={initvalues}")
+            log(f"sco_users: new_values={vals}")
+        else:
+            vals = {"roles_string": vals["roles_string"]}
+        the_user.from_dict(vals)
+        db.session.add(the_user)
+        db.session.commit()
+        flash(f"Utilisateur {user_name} modifié")
+        return flask.redirect(
+            url_for(
+                "users.user_info_page",
+                scodoc_dept=g.scodoc_dept,
+                user_name=user_name,
             )
+        )
 
-        else:  # création utilisateur
-            vals["roles_string"] = ",".join(vals["roles"])
-            # check identifiant
-            if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]):
-                msg = tf_error_message(
-                    "identifiant invalide (pas d'accents ni de caractères spéciaux)"
-                )
-                return render_template(
-                    "base.j2",
-                    content="\n".join(H) + msg + "\n" + tf[1],
-                    javascripts=["js/user_form.js"],
-                )
-            # Traitement initial (mode) : 3 cas
-            # cf énumération Mode
-            # A: envoi de welcome + procedure de reset
-            # B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
-            # C: Aucun envoi (mot de passe saisi dans le formulaire)
-            if vals["welcome"]:  # "Envoie un mail d'accueil" coché
-                if vals["reset_password"] and (
-                    (not ScoDocSiteConfig.get("cas_force"))
-                    or vals.get("cas_allow_scodoc_login", False)
-                ):
-                    # nb: si login scodoc non autorisé car CAS seul, n'envoie pas le mot de passe.
-                    mode = Mode.WELCOME_AND_CHANGE_PASSWORD
-                else:
-                    mode = Mode.WELCOME_ONLY
+    else:  # création utilisateur
+        vals["roles_string"] = ",".join(vals["roles"])
+        # check identifiant
+        if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]):
+            msg = tf_error_message(
+                "identifiant invalide (pas d'accents ni de caractères spéciaux)"
+            )
+            return render_template(
+                "base.j2",
+                content="\n".join(H) + msg + "\n" + tf[1],
+                javascripts=["js/user_form.js"],
+            )
+        # Traitement initial (mode) : 3 cas
+        # cf énumération Mode
+        # A: envoi de welcome + procedure de reset
+        # B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
+        # C: Aucun envoi (mot de passe saisi dans le formulaire)
+        if vals["welcome"]:  # "Envoie un mail d'accueil" coché
+            if vals["reset_password"] and (
+                (not ScoDocSiteConfig.get("cas_force"))
+                or vals.get("cas_allow_scodoc_login", False)
+            ):
+                # nb: si login scodoc non autorisé car CAS seul, n'envoie pas le mot de passe.
+                mode = Mode.WELCOME_AND_CHANGE_PASSWORD
             else:
-                mode = Mode.SILENT
+                mode = Mode.WELCOME_ONLY
+        else:
+            mode = Mode.SILENT
 
-            # check passwords
+        # check passwords
+        if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
+            vals["password"] = generate_password()
+        else:
+            if vals["password"]:
+                if vals["password"] != vals["password2"]:
+                    msg = tf_error_message(
+                        """Les deux mots de passes ne correspondent pas !"""
+                    )
+                    return render_template(
+                        "base.j2",
+                        content="\n".join(H) + msg + "\n" + tf[1],
+                        javascripts=["js/user_form.js"],
+                    )
+                if not is_valid_password(vals["password"]):
+                    msg = tf_error_message(
+                        """Mot de passe trop simple, recommencez !"""
+                    )
+                    return render_template(
+                        "base.j2",
+                        content="\n".join(H) + msg + "\n" + tf[1],
+                        javascripts=["js/user_form.js"],
+                    )
+        # Département:
+        if not can_choose_dept:
+            vals["dept"] = auth_dept
+        else:
+            if auth_dept:  # pas super-admin
+                if vals["dept"] not in selectable_dept_acronyms:
+                    raise ScoValueError("département invalide")
+        # ok, go
+        log(f"""sco_users: new_user {vals["user_name"]} by {current_user.user_name}""")
+        the_user = User(user_name=user_name)
+        the_user.from_dict(vals, new_user=True)
+        db.session.add(the_user)
+        db.session.commit()
+        # envoi éventuel d'un message
+        if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY:
             if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
-                vals["password"] = generate_password()
+                token = the_user.get_reset_password_token()
             else:
-                if vals["password"]:
-                    if vals["password"] != vals["password2"]:
-                        msg = tf_error_message(
-                            """Les deux mots de passes ne correspondent pas !"""
-                        )
-                        return render_template(
-                            "base.j2",
-                            content="\n".join(H) + msg + "\n" + tf[1],
-                            javascripts=["js/user_form.js"],
-                        )
-                    if not is_valid_password(vals["password"]):
-                        msg = tf_error_message(
-                            """Mot de passe trop simple, recommencez !"""
-                        )
-                        return render_template(
-                            "base.j2",
-                            content="\n".join(H) + msg + "\n" + tf[1],
-                            javascripts=["js/user_form.js"],
-                        )
-            # Département:
-            if not can_choose_dept:
-                vals["dept"] = auth_dept
-            else:
-                if auth_dept:  # pas super-admin
-                    if vals["dept"] not in selectable_dept_acronyms:
-                        raise ScoValueError("département invalide")
-            # ok, go
-            log(
-                f"""sco_users: new_user {vals["user_name"]} by {current_user.user_name}"""
+                token = None
+            cas_force = ScoDocSiteConfig.get("cas_force")
+            # Le from doit utiliser la préférence du département de l'utilisateur
+            email.send_email(
+                "[ScoDoc] Création de votre compte",
+                sender=email.get_from_addr(),
+                recipients=[the_user.email],
+                text_body=render_template(
+                    "email/welcome.txt",
+                    user=the_user,
+                    token=token,
+                    cas_force=cas_force,
+                ),
+                html_body=render_template(
+                    "email/welcome.j2",
+                    user=the_user,
+                    token=token,
+                    cas_force=cas_force,
+                ),
             )
-            the_user = User(user_name=user_name)
-            the_user.from_dict(vals, new_user=True)
-            db.session.add(the_user)
-            db.session.commit()
-            # envoi éventuel d'un message
-            if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY:
-                if mode == Mode.WELCOME_AND_CHANGE_PASSWORD:
-                    token = the_user.get_reset_password_token()
-                else:
-                    token = None
-                cas_force = ScoDocSiteConfig.get("cas_force")
-                # Le from doit utiliser la préférence du département de l'utilisateur
-                email.send_email(
-                    "[ScoDoc] Création de votre compte",
-                    sender=email.get_from_addr(),
-                    recipients=[the_user.email],
-                    text_body=render_template(
-                        "email/welcome.txt",
-                        user=the_user,
-                        token=token,
-                        cas_force=cas_force,
-                    ),
-                    html_body=render_template(
-                        "email/welcome.j2",
-                        user=the_user,
-                        token=token,
-                        cas_force=cas_force,
-                    ),
-                )
-                flash(f"Mail accueil envoyé à {the_user.email}")
-
-            flash("Nouvel utilisateur créé")
-            return flask.redirect(
-                url_for(
-                    "users.user_info_page",
-                    scodoc_dept=g.scodoc_dept,
-                    user_name=user_name,
-                )
+            flash(f"Mail accueil envoyé à {the_user.email}")
+
+        flash("Nouvel utilisateur créé")
+        return flask.redirect(
+            url_for(
+                "users.user_info_page",
+                scodoc_dept=g.scodoc_dept,
+                user_name=user_name,
             )
+        )
 
 
 @bp.route("/import_users_generate_excel_sample")
-- 
GitLab