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