From d7f4209a5a9d0a0bc63052f93d06eef20d5248df Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Fri, 1 Nov 2024 11:58:58 +0100
Subject: [PATCH] Comptes utilisateur: option pour forcer modif mot de passe.

---
 app/auth/cas.py                               |   2 +-
 app/auth/logic.py                             |  12 +-
 app/auth/models.py                            |  12 +-
 app/auth/routes.py                            |  21 +-
 app/but/bulletin_but.py                       |   4 +-
 app/models/but_refcomp.py                     |   2 +-
 app/models/formsemestre.py                    |  12 +-
 app/models/moduleimpls.py                     |  10 +
 app/scodoc/sco_archives_etud.py               |   6 +-
 app/scodoc/sco_bulletins.py                   |   2 +
 app/scodoc/sco_formsemestre_edit.py           |   9 -
 app/scodoc/sco_permissions_check.py           |   6 +-
 app/scodoc/sco_users.py                       |   4 +
 app/templates/auth/msg_change_password.j2     |  38 ++
 app/templates/auth/user_info_page.j2          |   3 +
 app/templates/babase.j2                       |   3 +
 app/templates/base.j2                         |   5 +-
 app/views/__init__.py                         |   2 +-
 app/views/users.py                            |  10 +
 .../bcd959a23aea_passwd_must_be_changed.py    |  34 ++
 .../results/formsemestre_resultat.json        | 480 +++++++++---------
 21 files changed, 408 insertions(+), 269 deletions(-)
 create mode 100644 app/templates/auth/msg_change_password.j2
 create mode 100644 migrations/versions/bcd959a23aea_passwd_must_be_changed.py

diff --git a/app/auth/cas.py b/app/auth/cas.py
index 268611469..c96e9689e 100644
--- a/app/auth/cas.py
+++ b/app/auth/cas.py
@@ -38,7 +38,7 @@ def after_cas_login():
                         flask.session["scodoc_cas_login_date"] = (
                             datetime.datetime.now().isoformat()
                         )
-                        user.cas_last_login = datetime.datetime.utcnow()
+                        user.cas_last_login = datetime.datetime.now()
                         if flask.session.get("CAS_EDT_ID"):
                             # essaie de récupérer l'edt_id s'il est présent
                             # cet ID peut être renvoyé par le CAS et extrait par ScoDoc
diff --git a/app/auth/logic.py b/app/auth/logic.py
index 496aea1d6..51f1d68df 100644
--- a/app/auth/logic.py
+++ b/app/auth/logic.py
@@ -9,8 +9,8 @@ from flask import current_app, g, redirect, request, url_for
 from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
 import flask_login
 
-from app import db, login
-from app.auth.models import User
+from app import db, log, login
+from app.auth.models import User, Role
 from app.models.config import ScoDocSiteConfig
 from app.scodoc.sco_utils import json_error
 
@@ -19,7 +19,7 @@ token_auth = HTTPTokenAuth()
 
 
 @basic_auth.verify_password
-def verify_password(username, password):
+def verify_password(username, password) -> User | None:
     """Verify password for this user
     Appelé lors d'une demande de jeton (normalement via la route /tokens)
     """
@@ -28,6 +28,7 @@ def verify_password(username, password):
         g.current_user = user
         # note: est aussi basic_auth.current_user()
         return user
+    return None
 
 
 @basic_auth.error_handler
@@ -61,7 +62,8 @@ def token_auth_error(status):
 
 
 @token_auth.get_user_roles
-def get_user_roles(user):
+def get_user_roles(user) -> list[Role]:
+    "list roles"
     return user.roles
 
 
@@ -82,7 +84,7 @@ def load_user_from_request(req: flask.Request) -> User:
 @login.unauthorized_handler
 def unauthorized():
     "flask-login: si pas autorisé, redirige vers page login, sauf si API"
-    if request.blueprint == "api" or request.blueprint == "apiweb":
+    if request.blueprint in ("api", "apiweb"):
         return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)")
     return redirect(url_for("auth.login"))
 
diff --git a/app/auth/models.py b/app/auth/models.py
index 0354e9288..b73190459 100644
--- a/app/auth/models.py
+++ b/app/auth/models.py
@@ -105,6 +105,9 @@ class User(UserMixin, ScoDocModel):
     date_modif_passwd = db.Column(db.DateTime, default=datetime.now)
     date_created = db.Column(db.DateTime, default=datetime.now)
     date_expiration = db.Column(db.DateTime, default=None)
+    passwd_must_be_changed = db.Column(
+        db.Boolean, nullable=False, server_default="false", default=False
+    )
     passwd_temp = db.Column(db.Boolean, default=False)
     """champ obsolete. Si connexion alors que passwd_temp est vrai,
     efface mot de passe et redirige vers accueil."""
@@ -185,6 +188,8 @@ class User(UserMixin, ScoDocModel):
         # La création d'un mot de passe efface l'éventuel mot de passe historique
         self.password_scodoc7 = None
         self.passwd_temp = False
+        # Retire le flag
+        self.passwd_must_be_changed = False
 
     def check_password(self, password: str) -> bool:
         """Check given password vs current one.
@@ -282,6 +287,7 @@ class User(UserMixin, ScoDocModel):
                 if self.date_modif_passwd
                 else None
             ),
+            "passwd_must_be_changed": self.passwd_must_be_changed,
             "date_created": (
                 self.date_created.isoformat() + "Z" if self.date_created else None
             ),
@@ -385,7 +391,7 @@ class User(UserMixin, ScoDocModel):
 
     def get_token(self, expires_in=3600):
         "Un jeton pour cet user. Stocké en base, non commité."
-        now = datetime.utcnow()
+        now = datetime.now()
         if self.token and self.token_expiration > now + timedelta(seconds=60):
             return self.token
         self.token = base64.b64encode(os.urandom(24)).decode("utf-8")
@@ -395,7 +401,7 @@ class User(UserMixin, ScoDocModel):
 
     def revoke_token(self):
         "Révoque le jeton de cet utilisateur"
-        self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
+        self.token_expiration = datetime.now() - timedelta(seconds=1)
 
     @staticmethod
     def check_token(token):
@@ -403,7 +409,7 @@ class User(UserMixin, ScoDocModel):
         and returns the user object.
         """
         user = User.query.filter_by(token=token).first()
-        if user is None or user.token_expiration < datetime.utcnow():
+        if user is None or user.token_expiration < datetime.now():
             return None
         return user
 
diff --git a/app/auth/routes.py b/app/auth/routes.py
index 778cf8e5e..e8283c1a4 100644
--- a/app/auth/routes.py
+++ b/app/auth/routes.py
@@ -4,7 +4,7 @@ auth.routes.py
 """
 
 import flask
-from flask import current_app, flash, render_template
+from flask import current_app, flash, g, render_template
 from flask import redirect, url_for, request
 from flask_login import login_user, current_user
 from sqlalchemy import func
@@ -23,6 +23,7 @@ from app.auth.email import send_password_reset_email
 from app.decorators import admin_required
 from app.forms.generic import SimpleConfirmationForm
 from app.models.config import ScoDocSiteConfig
+from app.models.departements import Departement
 from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
 from app.scodoc import sco_utils as scu
 
@@ -49,6 +50,24 @@ def _login_form():
         login_user(user, remember=form.remember_me.data)
 
         current_app.logger.info("login: success (%s)", form.user_name.data)
+
+        if user.passwd_must_be_changed:
+            # Mot de passe à changer à la première connexion
+            dept = user.dept or getattr(g, "scodoc_dept", None)
+            if not dept:
+                departement = db.session.query(Departement).first()
+                dept = departement.acronym
+            if dept:
+                # Redirect to the password change page
+                flash("Votre mot de passe doit être changé")
+                return redirect(
+                    url_for(
+                        "users.form_change_password",
+                        scodoc_dept=dept,
+                        user_name=user.user_name,
+                    )
+                )
+
         return form.redirect("scodoc.index")
 
     return render_template(
diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 4a6908ec3..cf706008b 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -407,7 +407,9 @@ class BulletinBUT:
         d = {
             "version": "0",
             "type": "BUT",
-            "date": datetime.datetime.utcnow().isoformat() + "Z",
+            "date": datetime.datetime.now(datetime.timezone.utc)
+            .astimezone()
+            .isoformat(),
             "publie": not formsemestre.bul_hide_xml,
             "etat_inscription": etud.inscription_etat(formsemestre.id),
             "etudiant": etud.to_dict_bul(),
diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py
index d89e19dab..69c3e0fc5 100644
--- a/app/models/but_refcomp.py
+++ b/app/models/but_refcomp.py
@@ -76,7 +76,7 @@ class ApcReferentielCompetences(models.ScoDocModel, XMLModel):
         "version": "version_orebut",
     }
     # ScoDoc specific fields:
-    scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
+    scodoc_date_loaded = db.Column(db.DateTime, default=datetime.now)
     scodoc_orig_filename = db.Column(db.Text())
     # Relations:
     competences = db.relationship(
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 863b5d8ce..dc08a847b 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -646,9 +646,11 @@ class FormSemestre(models.ScoDocModel):
         )
         return [db.session.get(ModuleImpl, modimpl_id) for modimpl_id in cursor]
 
-    def can_be_edited_by(self, user):
+    def can_be_edited_by(self, user: User):
         """Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
-        if not user.has_permission(Permission.EditFormSemestre):  # pas chef
+        if user.passwd_must_be_changed or not user.has_permission(
+            Permission.EditFormSemestre
+        ):  # pas chef
             if not self.resp_can_edit or user.id not in [
                 resp.id for resp in self.responsables
             ]:
@@ -897,6 +899,8 @@ class FormSemestre(models.ScoDocModel):
         if not self.etat:
             return False  # semestre verrouillé
         user = user or current_user
+        if user.passwd_must_be_changed:
+            return False
         if user.has_permission(Permission.EtudChangeGroups):
             return True  # typiquement admin, chef dept
         return self.est_responsable(user)
@@ -906,11 +910,15 @@ class FormSemestre(models.ScoDocModel):
         dans ce semestre: vérifie permission et verrouillage.
         """
         user = user or current_user
+        if user.passwd_must_be_changed:
+            return False
         return self.etat and self.est_chef_or_diretud(user)
 
     def can_edit_pv(self, user: User = None):
         "Vrai si utilisateur (par def. current) peut editer un PV de jury de ce semestre"
         user = user or current_user
+        if user.passwd_must_be_changed:
+            return False
         # Autorise les secrétariats, repérés via la permission EtudChangeAdr
         return self.est_chef_or_diretud(user) or user.has_permission(
             Permission.EtudChangeAdr
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index c3a3c2698..a3ce27a72 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -199,6 +199,8 @@ class ModuleImpl(ScoDocModel):
         """True if this user can create, delete or edit and evaluation in this modimpl
         (nb: n'implique pas le droit de saisir ou modifier des notes)
         """
+        if user.passwd_must_be_changed:
+            return False
         # acces pour resp. moduleimpl et resp. form semestre (dir etud)
         if (
             user.has_permission(Permission.EditAllEvals)
@@ -222,6 +224,8 @@ class ModuleImpl(ScoDocModel):
         # was sco_permissions_check.can_edit_notes
         from app.scodoc import sco_cursus_dut
 
+        if user.passwd_must_be_changed:
+            return False
         if not self.formsemestre.etat:
             return False  # semestre verrouillé
         is_dir_etud = user.id in (u.id for u in self.formsemestre.responsables)
@@ -247,6 +251,8 @@ class ModuleImpl(ScoDocModel):
             if raise_exc:
                 raise ScoLockedSemError("Modification impossible: semestre verrouille")
             return False
+        if user.passwd_must_be_changed:
+            return False
         # -- check access
         # admin ou resp. semestre avec flag resp_can_change_resp
         if user.has_permission(Permission.EditFormSemestre):
@@ -264,6 +270,8 @@ class ModuleImpl(ScoDocModel):
         if user is None, current user.
         """
         user = current_user if user is None else user
+        if user.passwd_must_be_changed:
+            return False
         if not self.formsemestre.etat:
             if raise_exc:
                 raise ScoLockedSemError("Modification impossible: semestre verrouille")
@@ -285,6 +293,8 @@ class ModuleImpl(ScoDocModel):
         Autorise ScoEtudInscrit ou responsables semestre.
         """
         user = current_user if user is None else user
+        if user.passwd_must_be_changed:
+            return False
         if not self.formsemestre.etat:
             if raise_exc:
                 raise ScoLockedSemError("Modification impossible: semestre verrouille")
diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py
index 0067be0af..be0b70523 100644
--- a/app/scodoc/sco_archives_etud.py
+++ b/app/scodoc/sco_archives_etud.py
@@ -54,9 +54,11 @@ class EtudsArchiver(sco_archives.BaseArchiver):
 ETUDS_ARCHIVER = EtudsArchiver()
 
 
-def can_edit_etud_archive(authuser):
+def can_edit_etud_archive(user):
     """True si l'utilisateur peut modifier les archives etudiantes"""
-    return authuser.has_permission(Permission.EtudAddAnnotations)
+    if user.passwd_must_be_changed:
+        return False
+    return user.has_permission(Permission.EtudAddAnnotations)
 
 
 def etud_list_archives_html(etud: Identite):
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index e59249926..5279ab98b 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -1001,6 +1001,8 @@ def formsemestre_bulletinetud(
 def can_send_bulletin_by_mail(formsemestre_id):
     """True if current user is allowed to send a bulletin (pdf) by mail"""
     sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+    if current_user.passwd_must_be_changed:
+        return False
     return (
         sco_preferences.get_preference("bul_mail_allowed_for_all", formsemestre_id)
         or current_user.has_permission(Permission.EditFormSemestre)
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 74f1d7481..ee7a133a3 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -134,15 +134,6 @@ def formsemestre_editwithmodules(formsemestre_id: int):
     )
 
 
-def can_edit_sem(formsemestre_id: int = None, sem=None):
-    """Return sem if user can edit it, False otherwise"""
-    sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
-    if not current_user.has_permission(Permission.EditFormSemestre):  # pas chef
-        if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
-            return False
-    return sem
-
-
 RESP_FIELDS = [
     "responsable_id",
     "responsable_id2",
diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py
index 6f98aafd2..4145b7e06 100644
--- a/app/scodoc/sco_permissions_check.py
+++ b/app/scodoc/sco_permissions_check.py
@@ -17,6 +17,8 @@ def can_suppress_annotation(annotation_id):
     Seuls l'auteur de l'annotation et le chef de dept peuvent supprimer
     une annotation.
     """
+    if current_user.passwd_must_be_changed:
+        return False
     annotation = (
         EtudAnnotation.query.filter_by(id=annotation_id)
         .join(Identite)
@@ -30,8 +32,10 @@ def can_suppress_annotation(annotation_id):
     )
 
 
-def can_edit_suivi():
+def can_edit_suivi() -> bool:
     """Vrai si l'utilisateur peut modifier les informations de suivi sur la page etud" """
+    if current_user.passwd_must_be_changed:
+        return False
     return current_user.has_permission(Permission.EtudChangeAdr)
 
 
diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py
index d31c08380..db27e0c76 100644
--- a/app/scodoc/sco_users.py
+++ b/app/scodoc/sco_users.py
@@ -184,9 +184,11 @@ def list_users(
             if not current_user.is_administrator():
                 # si non super-admin, ne donne pas la date exacte de derniere connexion
                 d["last_seen"] = _approximate_date(u.last_seen)
+            d["passwd_must_be_changed"] = "OUI" if d["passwd_must_be_changed"] else ""
         else:
             d["date_modif_passwd"] = "(non visible)"
             d["non_migre"] = ""
+            d["passwd_must_be_changed"] = ""
         if detail_roles:
             d["roles_set"] = {
                 f"{r.role.name or ''}_{r.dept or ''}" for r in u.user_roles
@@ -209,6 +211,7 @@ def list_users(
         "roles_string",
         "date_expiration",
         "date_modif_passwd",
+        "passwd_must_be_changed",
         "non_migre",
         "status_txt",
     ]
@@ -240,6 +243,7 @@ def list_users(
         "roles_string": "Rôles",
         "date_expiration": "Expiration",
         "date_modif_passwd": "Modif. mot de passe",
+        "passwd_must_be_changed": "À changer",
         "last_seen": "Dernière cnx.",
         "non_migre": "Non migré (!)",
         "status_txt": "Etat",
diff --git a/app/templates/auth/msg_change_password.j2 b/app/templates/auth/msg_change_password.j2
new file mode 100644
index 000000000..ed572907b
--- /dev/null
+++ b/app/templates/auth/msg_change_password.j2
@@ -0,0 +1,38 @@
+<style>
+div.msg-change-passwd {
+    border: 3px solid white;
+    border-radius: 8px;
+    padding: 16px;
+    width: fit-content;
+    margin-left: auto;
+    margin-right: auto;
+    margin-top: 28px;
+    margin-bottom: 28px;
+}
+div.msg-change-passwd, div.msg-change-passwd a {
+    font-size: 36px;
+    font-weight: bold;
+    background-color: red;
+    color: white;
+}
+div.msg-change-passwd a, div.msg-change-passwd a:visited {
+    text-decoration: underline;
+}
+</style>
+<div class="msg-change-passwd">
+Vous devez
+    {% if current_user.dept %}
+        <a class="nav-link" href="{{
+            url_for(
+                        'users.form_change_password',
+                        scodoc_dept=current_user.dept,
+                        user_name=current_user.user_name
+                    )
+        }}">
+    {% endif %}
+        changer votre mot de passe
+    {% if current_user.dept %}
+        </a>
+    {% endif %}
+    !
+</div>
diff --git a/app/templates/auth/user_info_page.j2 b/app/templates/auth/user_info_page.j2
index b3957202a..6b36704a5 100644
--- a/app/templates/auth/user_info_page.j2
+++ b/app/templates/auth/user_info_page.j2
@@ -18,6 +18,9 @@
     <br>
     <b>Nom :</b> {{user.nom or ""}}<br>
     <b>Prénom :</b> {{user.prenom or ""}}<br>
+    {% if user.passwd_must_be_changed %}
+        <div style="color:white; background-color: red; padding:8px; margin-top: 4px; width: fit-content;">mot de passe à changer</div>
+    {% endif %}
     <b>Mail :</b> {{user.email}}<br>
     <b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br>
     <b>Identifiant EDT:</b> {{user.edt_id or ""}}<br>
diff --git a/app/templates/babase.j2 b/app/templates/babase.j2
index b48669535..ef538ca1a 100644
--- a/app/templates/babase.j2
+++ b/app/templates/babase.j2
@@ -25,6 +25,9 @@
     {% block navbar %}
     {%- endblock navbar %}
     <div id="sco_msg" class="head_message"></div>
+    {% if current_user and current_user.passwd_must_be_changed %}
+      {% include "auth/msg_change_password.j2" %}
+    {% endif %}
     {% block content -%}
     {%- endblock content %}
 
diff --git a/app/templates/base.j2 b/app/templates/base.j2
index ed1ff28eb..dcde83581 100644
--- a/app/templates/base.j2
+++ b/app/templates/base.j2
@@ -46,7 +46,8 @@
                 {% if current_user.is_anonymous %}
                 <li class="nav-item"><a class="nav-link" href="{{ url_for('auth.login') }}">connexion</a></li>
                 {% else %}
-                <li class="nav-item">{% if current_user.dept %}
+                <li class="nav-item">
+                    {% if current_user.dept %}
                     <a class="nav-link" href="{{ url_for('users.user_info_page', scodoc_dept=current_user.dept, user_name=current_user.user_name )
                         }}">{{current_user.user_name}} ({{current_user.dept}})</a>
                     {% else %}
@@ -89,7 +90,7 @@
 <script>
     const SCO_URL = "{% if g.scodoc_dept %}{{
         url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}{% endif %}";
-    
+
     document.querySelector('.navbar-toggler').addEventListener('click', function() {
         document.querySelector('.navbar-collapse').classList.toggle('show');
     });
diff --git a/app/views/__init__.py b/app/views/__init__.py
index 65d2b9e11..7193d7f5e 100644
--- a/app/views/__init__.py
+++ b/app/views/__init__.py
@@ -37,7 +37,7 @@ def start_scodoc_request():
     # current_app.logger.info(f"start_scodoc_request")
     ndb.open_db_connection()
     if current_user and current_user.is_authenticated:
-        current_user.last_seen = datetime.datetime.utcnow()
+        current_user.last_seen = datetime.datetime.now()
         db.session.commit()
     # caches locaux (durée de vie=la requête en cours)
     g.stored_get_formsemestre = {}
diff --git a/app/views/users.py b/app/views/users.py
index df8368de4..3ffe57938 100644
--- a/app/views/users.py
+++ b/app/views/users.py
@@ -334,6 +334,16 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
                 },
             )
         )
+        descr.append(
+            (
+                "passwd_must_be_changed",
+                {
+                    "title": "Force à changer le mot de passe",
+                    "input_type": "boolcheckbox",
+                    "explanation": """ à la première connexion.""",
+                },
+            )
+        )
     if not edit:
         descr += [
             (
diff --git a/migrations/versions/bcd959a23aea_passwd_must_be_changed.py b/migrations/versions/bcd959a23aea_passwd_must_be_changed.py
new file mode 100644
index 000000000..6eedcdf0b
--- /dev/null
+++ b/migrations/versions/bcd959a23aea_passwd_must_be_changed.py
@@ -0,0 +1,34 @@
+"""passwd_must_be_changed
+
+Revision ID: bcd959a23aea
+Revises: 2640b7686de6
+Create Date: 2024-11-01 09:51:01.299407
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "bcd959a23aea"
+down_revision = "2640b7686de6"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    with op.batch_alter_table("user", schema=None) as batch_op:
+        batch_op.add_column(
+            sa.Column(
+                "passwd_must_be_changed",
+                sa.Boolean(),
+                server_default="false",
+                nullable=False,
+            )
+        )
+
+
+def downgrade():
+    with op.batch_alter_table("user", schema=None) as batch_op:
+        batch_op.drop_column("passwd_must_be_changed")
diff --git a/tests/ressources/results/formsemestre_resultat.json b/tests/ressources/results/formsemestre_resultat.json
index d6187aebc..f3438baf2 100644
--- a/tests/ressources/results/formsemestre_resultat.json
+++ b/tests/ressources/results/formsemestre_resultat.json
@@ -5,27 +5,13 @@
         "rang": "1",
         "civilite_str": "Mme",
         "nom_disp": "BONHOMME",
-        "prenom": "MADELEINE",
+        "prenom": "Madeleine",
         "nom_short": "BONHOMME Ma.",
         "partitions": {
             "1": 1
         },
         "sort_key": "bonhomme;madeleine",
         "moy_gen": "14.36",
-        "moy_ue_1": "14.94",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "11.97",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "15.71",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "10.66",
-        "moy_res_12_1": "12.50",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "18.72",
-        "moy_sae_7_1": "14.69",
         "moy_ue_2": "11.17",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -49,6 +35,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "17.83",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "14.94",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "11.97",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "15.71",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "10.66",
+        "moy_res_12_1": "12.50",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "18.72",
+        "moy_sae_7_1": "14.69",
         "ues_validables": "3/3",
         "nbabs": 1,
         "nbabsjust": 0,
@@ -65,27 +65,13 @@
         "rang": "2",
         "civilite_str": "M.",
         "nom_disp": "JAMES",
-        "prenom": "JACQUES",
+        "prenom": "Jacques",
         "nom_short": "JAMES Ja.",
         "partitions": {
             "1": 1
         },
         "sort_key": "james;jacques",
         "moy_gen": "12.67",
-        "moy_ue_1": "13.51",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "03.27",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "13.05",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "04.35",
-        "moy_res_12_1": "18.85",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "~",
-        "moy_sae_7_1": "17.07",
         "moy_ue_2": "14.24",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -109,6 +95,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "10.74",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "13.51",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "03.27",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "13.05",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "04.35",
+        "moy_res_12_1": "18.85",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "~",
+        "moy_sae_7_1": "17.07",
         "ues_validables": "3/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -125,27 +125,13 @@
         "rang": "3",
         "civilite_str": "",
         "nom_disp": "THIBAUD",
-        "prenom": "MAXIME",
+        "prenom": "Maxime",
         "nom_short": "THIBAUD Ma.",
         "partitions": {
             "1": 1
         },
         "sort_key": "thibaud;maxime",
         "moy_gen": "12.02",
-        "moy_ue_1": "14.34",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "17.68",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "18.31",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "18.97",
-        "moy_res_12_1": "05.46",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "13.02",
-        "moy_sae_7_1": "14.11",
         "moy_ue_2": "09.89",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -169,6 +155,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "05.70",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "14.34",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "17.68",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "18.31",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "18.97",
+        "moy_res_12_1": "05.46",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "13.02",
+        "moy_sae_7_1": "14.11",
         "ues_validables": "2/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -185,27 +185,13 @@
         "rang": "4",
         "civilite_str": "",
         "nom_disp": "ROYER",
-        "prenom": "CAMILLE",
+        "prenom": "Camille",
         "nom_short": "ROYER Ca.",
         "partitions": {
             "1": 1
         },
         "sort_key": "royer;camille",
         "moy_gen": "11.88",
-        "moy_ue_1": "07.09",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "04.07",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "17.62",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "16.57",
-        "moy_res_12_1": "18.61",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "14.13",
-        "moy_sae_7_1": "00.53",
         "moy_ue_2": "17.35",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -229,6 +215,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "10.52",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "07.09",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "04.07",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "17.62",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "16.57",
+        "moy_res_12_1": "18.61",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "14.13",
+        "moy_sae_7_1": "00.53",
         "ues_validables": "2/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -245,27 +245,13 @@
         "rang": "5",
         "civilite_str": "M.",
         "nom_disp": "GODIN",
-        "prenom": "CLAUDE",
+        "prenom": "Claude",
         "nom_short": "GODIN Cl.",
         "partitions": {
             "1": 1
         },
         "sort_key": "godin;claude",
         "moy_gen": "10.52",
-        "moy_ue_1": "08.93",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "07.77",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "00.48",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "08.95",
-        "moy_res_12_1": "18.10",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "14.29",
-        "moy_sae_7_1": "06.89",
         "moy_ue_2": "16.04",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -289,6 +275,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "11.09",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "08.93",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "07.77",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "00.48",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "08.95",
+        "moy_res_12_1": "18.10",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "14.29",
+        "moy_sae_7_1": "06.89",
         "ues_validables": "1/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -305,27 +305,13 @@
         "rang": "6",
         "civilite_str": "M.",
         "nom_disp": "CONSTANT",
-        "prenom": "PATRICK",
+        "prenom": "Patrick",
         "nom_short": "CONSTANT Pa.",
         "partitions": {
             "1": 1
         },
         "sort_key": "constant;patrick",
         "moy_gen": "10.04",
-        "moy_ue_1": "13.06",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "05.84",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "11.44",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "14.04",
-        "moy_res_12_1": "13.28",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "09.82",
-        "moy_sae_7_1": "17.46",
         "moy_ue_2": "10.62",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -349,6 +335,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "01.55",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "13.06",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "05.84",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "11.44",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "14.04",
+        "moy_res_12_1": "13.28",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "09.82",
+        "moy_sae_7_1": "17.46",
         "ues_validables": "2/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -365,27 +365,13 @@
         "rang": "7",
         "civilite_str": "",
         "nom_disp": "TOUSSAINT",
-        "prenom": "ALIX",
+        "prenom": "Alix",
         "nom_short": "TOUSSAINT Al.",
         "partitions": {
             "1": 1
         },
         "sort_key": "toussaint;alix",
         "moy_gen": "08.59",
-        "moy_ue_1": "07.24",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "11.90",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "00.47",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "18.66",
-        "moy_res_12_1": "18.02",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "~",
-        "moy_sae_7_1": "04.46",
         "moy_ue_2": "13.93",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -409,6 +395,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "05.17",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "07.24",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "11.90",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "00.47",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "18.66",
+        "moy_res_12_1": "18.02",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "~",
+        "moy_sae_7_1": "04.46",
         "ues_validables": "1/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -425,27 +425,13 @@
         "rang": "8",
         "civilite_str": "",
         "nom_disp": "DENIS",
-        "prenom": "MAXIME",
+        "prenom": "Maxime",
         "nom_short": "DENIS Ma.",
         "partitions": {
             "1": 1
         },
         "sort_key": "denis;maxime",
         "moy_gen": "07.21",
-        "moy_ue_1": "06.86",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "~",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "10.06",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "11.75",
-        "moy_res_12_1": "01.88",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "14.55",
-        "moy_sae_7_1": "03.02",
         "moy_ue_2": "08.84",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -469,6 +455,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "03.32",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "06.86",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "~",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "10.06",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "11.75",
+        "moy_res_12_1": "01.88",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "14.55",
+        "moy_sae_7_1": "03.02",
         "ues_validables": "0/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -485,27 +485,13 @@
         "rang": "9",
         "civilite_str": "Mme",
         "nom_disp": "WALTER",
-        "prenom": "SIMONE",
+        "prenom": "Simone",
         "nom_short": "WALTER Si.",
         "partitions": {
             "1": 1
         },
         "sort_key": "walter;simone",
         "moy_gen": "07.02",
-        "moy_ue_1": "06.82",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "16.91",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "12.84",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "13.08",
-        "moy_res_12_1": "10.63",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "06.28",
-        "moy_sae_7_1": "01.36",
         "moy_ue_2": "07.96",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -529,6 +515,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "02.10",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "06.82",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "16.91",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "12.84",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "13.08",
+        "moy_res_12_1": "10.63",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "06.28",
+        "moy_sae_7_1": "01.36",
         "ues_validables": "0/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -545,27 +545,13 @@
         "rang": "10",
         "civilite_str": "",
         "nom_disp": "GROSS",
-        "prenom": "SACHA",
+        "prenom": "Sacha",
         "nom_short": "GROSS Sa.",
         "partitions": {
             "1": 1
         },
         "sort_key": "gross;sacha",
         "moy_gen": "05.31",
-        "moy_ue_1": "03.73",
-        "moy_res_1_1": "~",
-        "moy_res_3_1": "~",
-        "moy_res_4_1": "~",
-        "moy_res_5_1": "~",
-        "moy_res_6_1": "~",
-        "moy_res_18_1": "03.04",
-        "moy_res_10_1": "~",
-        "moy_res_11_1": "~",
-        "moy_res_20_1": "04.89",
-        "moy_res_12_1": "09.88",
-        "moy_res_13_1": "~",
-        "moy_sae_2_1": "~",
-        "moy_sae_7_1": "02.85",
         "moy_ue_2": "07.13",
         "moy_res_1_2": "~",
         "moy_res_4_2": "~",
@@ -589,6 +575,20 @@
         "moy_res_21_3": "~",
         "moy_sae_14_3": "07.17",
         "moy_sae_15_3": "~",
+        "moy_ue_1": "03.73",
+        "moy_res_1_1": "~",
+        "moy_res_3_1": "~",
+        "moy_res_4_1": "~",
+        "moy_res_5_1": "~",
+        "moy_res_6_1": "~",
+        "moy_res_18_1": "03.04",
+        "moy_res_10_1": "~",
+        "moy_res_11_1": "~",
+        "moy_res_20_1": "04.89",
+        "moy_res_12_1": "09.88",
+        "moy_res_13_1": "~",
+        "moy_sae_2_1": "~",
+        "moy_sae_7_1": "02.85",
         "ues_validables": "0/3",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -605,27 +605,13 @@
         "rang": "11 ex",
         "civilite_str": "M.",
         "nom_disp": "BARTHELEMY",
-        "prenom": "G\u00c9RARD",
+        "prenom": "G\u00e9rard",
         "nom_short": "BARTHELEMY G\u00e9.",
         "partitions": {
             "1": 1
         },
         "sort_key": "barthelemy;gerard",
         "moy_gen": "",
-        "moy_ue_1": "",
-        "moy_res_1_1": "",
-        "moy_res_3_1": "",
-        "moy_res_4_1": "",
-        "moy_res_5_1": "",
-        "moy_res_6_1": "",
-        "moy_res_18_1": "",
-        "moy_res_10_1": "",
-        "moy_res_11_1": "",
-        "moy_res_20_1": "",
-        "moy_res_12_1": "",
-        "moy_res_13_1": "",
-        "moy_sae_2_1": "",
-        "moy_sae_7_1": "",
         "moy_ue_2": "",
         "moy_res_1_2": "",
         "moy_res_4_2": "",
@@ -649,6 +635,20 @@
         "moy_res_21_3": "",
         "moy_sae_14_3": "",
         "moy_sae_15_3": "",
+        "moy_ue_1": "",
+        "moy_res_1_1": "",
+        "moy_res_3_1": "",
+        "moy_res_4_1": "",
+        "moy_res_5_1": "",
+        "moy_res_6_1": "",
+        "moy_res_18_1": "",
+        "moy_res_10_1": "",
+        "moy_res_11_1": "",
+        "moy_res_20_1": "",
+        "moy_res_12_1": "",
+        "moy_res_13_1": "",
+        "moy_sae_2_1": "",
+        "moy_sae_7_1": "",
         "ues_validables": "",
         "nbabs": 2,
         "nbabsjust": 0,
@@ -665,27 +665,13 @@
         "rang": "11 ex",
         "civilite_str": "Mme",
         "nom_disp": "MILLOT",
-        "prenom": "FRAN\u00c7OISE",
+        "prenom": "Fran\u00e7oise",
         "nom_short": "MILLOT Fr.",
         "partitions": {
             "1": 1
         },
         "sort_key": "millot;francoise",
         "moy_gen": "",
-        "moy_ue_1": "",
-        "moy_res_1_1": "",
-        "moy_res_3_1": "",
-        "moy_res_4_1": "",
-        "moy_res_5_1": "",
-        "moy_res_6_1": "",
-        "moy_res_18_1": "",
-        "moy_res_10_1": "",
-        "moy_res_11_1": "",
-        "moy_res_20_1": "",
-        "moy_res_12_1": "",
-        "moy_res_13_1": "",
-        "moy_sae_2_1": "",
-        "moy_sae_7_1": "",
         "moy_ue_2": "",
         "moy_res_1_2": "",
         "moy_res_4_2": "",
@@ -709,6 +695,20 @@
         "moy_res_21_3": "",
         "moy_sae_14_3": "",
         "moy_sae_15_3": "",
+        "moy_ue_1": "",
+        "moy_res_1_1": "",
+        "moy_res_3_1": "",
+        "moy_res_4_1": "",
+        "moy_res_5_1": "",
+        "moy_res_6_1": "",
+        "moy_res_18_1": "",
+        "moy_res_10_1": "",
+        "moy_res_11_1": "",
+        "moy_res_20_1": "",
+        "moy_res_12_1": "",
+        "moy_res_13_1": "",
+        "moy_sae_2_1": "",
+        "moy_sae_7_1": "",
         "ues_validables": "",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -725,27 +725,13 @@
         "rang": "11 ex",
         "civilite_str": "M.",
         "nom_disp": "BENOIT",
-        "prenom": "EMMANUEL",
+        "prenom": "Emmanuel",
         "nom_short": "BENOIT Em.",
         "partitions": {
             "1": 1
         },
         "sort_key": "benoit;emmanuel",
         "moy_gen": "",
-        "moy_ue_1": "",
-        "moy_res_1_1": "",
-        "moy_res_3_1": "",
-        "moy_res_4_1": "",
-        "moy_res_5_1": "",
-        "moy_res_6_1": "",
-        "moy_res_18_1": "",
-        "moy_res_10_1": "",
-        "moy_res_11_1": "",
-        "moy_res_20_1": "",
-        "moy_res_12_1": "",
-        "moy_res_13_1": "",
-        "moy_sae_2_1": "",
-        "moy_sae_7_1": "",
         "moy_ue_2": "",
         "moy_res_1_2": "",
         "moy_res_4_2": "",
@@ -769,6 +755,20 @@
         "moy_res_21_3": "",
         "moy_sae_14_3": "",
         "moy_sae_15_3": "",
+        "moy_ue_1": "",
+        "moy_res_1_1": "",
+        "moy_res_3_1": "",
+        "moy_res_4_1": "",
+        "moy_res_5_1": "",
+        "moy_res_6_1": "",
+        "moy_res_18_1": "",
+        "moy_res_10_1": "",
+        "moy_res_11_1": "",
+        "moy_res_20_1": "",
+        "moy_res_12_1": "",
+        "moy_res_13_1": "",
+        "moy_sae_2_1": "",
+        "moy_sae_7_1": "",
         "ues_validables": "",
         "nbabs": 2,
         "nbabsjust": 0,
@@ -785,27 +785,13 @@
         "rang": "11 ex",
         "civilite_str": "Mme",
         "nom_disp": "LECOCQ",
-        "prenom": "MARGUERITE",
+        "prenom": "Marguerite",
         "nom_short": "LECOCQ Ma.",
         "partitions": {
             "1": 1
         },
         "sort_key": "lecocq;marguerite",
         "moy_gen": "",
-        "moy_ue_1": "",
-        "moy_res_1_1": "",
-        "moy_res_3_1": "",
-        "moy_res_4_1": "",
-        "moy_res_5_1": "",
-        "moy_res_6_1": "",
-        "moy_res_18_1": "",
-        "moy_res_10_1": "",
-        "moy_res_11_1": "",
-        "moy_res_20_1": "",
-        "moy_res_12_1": "",
-        "moy_res_13_1": "",
-        "moy_sae_2_1": "",
-        "moy_sae_7_1": "",
         "moy_ue_2": "",
         "moy_res_1_2": "",
         "moy_res_4_2": "",
@@ -829,6 +815,20 @@
         "moy_res_21_3": "",
         "moy_sae_14_3": "",
         "moy_sae_15_3": "",
+        "moy_ue_1": "",
+        "moy_res_1_1": "",
+        "moy_res_3_1": "",
+        "moy_res_4_1": "",
+        "moy_res_5_1": "",
+        "moy_res_6_1": "",
+        "moy_res_18_1": "",
+        "moy_res_10_1": "",
+        "moy_res_11_1": "",
+        "moy_res_20_1": "",
+        "moy_res_12_1": "",
+        "moy_res_13_1": "",
+        "moy_sae_2_1": "",
+        "moy_sae_7_1": "",
         "ues_validables": "",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -845,27 +845,13 @@
         "rang": "11 ex",
         "civilite_str": "M.",
         "nom_disp": "ROUSSET",
-        "prenom": "DERC'HEN",
+        "prenom": "Derc'hen",
         "nom_short": "ROUSSET De.",
         "partitions": {
             "1": 1
         },
         "sort_key": "rousset;derchen",
         "moy_gen": "",
-        "moy_ue_1": "",
-        "moy_res_1_1": "",
-        "moy_res_3_1": "",
-        "moy_res_4_1": "",
-        "moy_res_5_1": "",
-        "moy_res_6_1": "",
-        "moy_res_18_1": "",
-        "moy_res_10_1": "",
-        "moy_res_11_1": "",
-        "moy_res_20_1": "",
-        "moy_res_12_1": "",
-        "moy_res_13_1": "",
-        "moy_sae_2_1": "",
-        "moy_sae_7_1": "",
         "moy_ue_2": "",
         "moy_res_1_2": "",
         "moy_res_4_2": "",
@@ -889,6 +875,20 @@
         "moy_res_21_3": "",
         "moy_sae_14_3": "",
         "moy_sae_15_3": "",
+        "moy_ue_1": "",
+        "moy_res_1_1": "",
+        "moy_res_3_1": "",
+        "moy_res_4_1": "",
+        "moy_res_5_1": "",
+        "moy_res_6_1": "",
+        "moy_res_18_1": "",
+        "moy_res_10_1": "",
+        "moy_res_11_1": "",
+        "moy_res_20_1": "",
+        "moy_res_12_1": "",
+        "moy_res_13_1": "",
+        "moy_sae_2_1": "",
+        "moy_sae_7_1": "",
         "ues_validables": "",
         "nbabs": 0,
         "nbabsjust": 0,
@@ -905,27 +905,13 @@
         "rang": "11 ex",
         "civilite_str": "",
         "nom_disp": "MORAND",
-        "prenom": "CAMILLE",
+        "prenom": "Camille",
         "nom_short": "MORAND Ca.",
         "partitions": {
             "1": 1
         },
         "sort_key": "morand;camille",
         "moy_gen": "",
-        "moy_ue_1": "",
-        "moy_res_1_1": "",
-        "moy_res_3_1": "",
-        "moy_res_4_1": "",
-        "moy_res_5_1": "",
-        "moy_res_6_1": "",
-        "moy_res_18_1": "",
-        "moy_res_10_1": "",
-        "moy_res_11_1": "",
-        "moy_res_20_1": "",
-        "moy_res_12_1": "",
-        "moy_res_13_1": "",
-        "moy_sae_2_1": "",
-        "moy_sae_7_1": "",
         "moy_ue_2": "",
         "moy_res_1_2": "",
         "moy_res_4_2": "",
@@ -949,6 +935,20 @@
         "moy_res_21_3": "",
         "moy_sae_14_3": "",
         "moy_sae_15_3": "",
+        "moy_ue_1": "",
+        "moy_res_1_1": "",
+        "moy_res_3_1": "",
+        "moy_res_4_1": "",
+        "moy_res_5_1": "",
+        "moy_res_6_1": "",
+        "moy_res_18_1": "",
+        "moy_res_10_1": "",
+        "moy_res_11_1": "",
+        "moy_res_20_1": "",
+        "moy_res_12_1": "",
+        "moy_res_13_1": "",
+        "moy_sae_2_1": "",
+        "moy_sae_7_1": "",
         "ues_validables": "",
         "nbabs": 1,
         "nbabsjust": 0,
-- 
GitLab