Comptes utilisateur: option pour forcer modif mot de passe.

This commit is contained in:
Emmanuel Viennet 2024-11-01 11:58:58 +01:00
parent 83a5855f3d
commit d7f4209a5a
21 changed files with 408 additions and 269 deletions

View File

@ -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

View File

@ -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"))

View File

@ -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

View File

@ -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(

View File

@ -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(),

View File

@ -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(

View File

@ -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

View File

@ -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")

View File

@ -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):

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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');
});

View File

@ -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 = {}

View File

@ -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 += [
(

View File

@ -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")

View File

@ -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,