CAS: export/import config comptes via Excel

This commit is contained in:
Emmanuel Viennet 2023-03-02 22:55:25 +01:00
parent 0c0d43d075
commit 3edf34dfee
15 changed files with 380 additions and 106 deletions

View File

@ -6,13 +6,15 @@ import datetime
import flask import flask
from flask import current_app, flash, url_for from flask import current_app, flash, url_for
from flask_login import login_user from flask_login import current_user, login_user
from app import db from app import db
from app.auth import bp from app.auth import bp
from app.auth.models import User from app.auth.models import User
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_excel
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
import app.scodoc.sco_utils as scu
# after_cas_login/after_cas_logout : routes appelées par redirect depuis le serveur CAS. # after_cas_login/after_cas_logout : routes appelées par redirect depuis le serveur CAS.
@ -94,3 +96,133 @@ def set_cas_configuration(app: flask.app.Flask = None):
app.config.pop("CAS_AFTER_LOGOUT", None) app.config.pop("CAS_AFTER_LOGOUT", None)
app.config.pop("CAS_SSL_VERIFY", None) app.config.pop("CAS_SSL_VERIFY", None)
app.config.pop("CAS_SSL_CERTIFICATE", None) app.config.pop("CAS_SSL_CERTIFICATE", None)
CAS_USER_INFO_IDS = (
"user_name",
"nom",
"prenom",
"email",
"roles_string",
"active",
"dept",
"cas_id",
"cas_allow_login",
"cas_allow_scodoc_login",
)
CAS_USER_INFO_COMMENTS = (
"""user_name:
L'identifiant (login).
""",
"",
"",
"",
"Pour info: 0 si compte inactif",
"""Pour info: roles:
chaînes séparées par _:
1. Le rôle (Ens, Secr ou Admin)
2. Le département (en majuscule)
""",
"""dept:
Le département d'appartenance de l'utilisateur. Vide si l'utilisateur
intervient dans plusieurs départements.
""",
"""cas_id:
identifiant de l'utilisateur sur CAS (requis pour CAS).
""",
"""cas_allow_login:
autorise la connexion via CAS (optionnel, faux par défaut)
""",
"""cas_allow_scodoc_login
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
""",
)
def cas_users_generate_excel_sample() -> bytes:
"""generate an excel document suitable to import users CAS information"""
style = sco_excel.excel_make_style(bold=True)
titles = CAS_USER_INFO_IDS
titles_styles = [style] * len(titles)
# Extrait tous les utilisateurs (tous dept et statuts)
rows = []
for user in User.query.order_by(User.user_name):
u_dict = user.to_dict()
rows.append([u_dict.get(k) for k in CAS_USER_INFO_IDS])
return sco_excel.excel_simple_table(
lines=rows,
titles=titles,
titles_styles=titles_styles,
sheet_name="Utilisateurs ScoDoc",
comments=CAS_USER_INFO_COMMENTS,
)
def cas_users_import_excel_file(datafile) -> int:
"""
Import users CAS configuration from Excel file.
May change cas_id, cas_allow_login, cas_allow_scodoc_login
:param datafile: stream to be imported
:return: nb de comptes utilisateurs modifiés
"""
from app.scodoc import sco_import_users
if not current_user.is_administrator():
raise AccessDenied(f"invalid user ({current_user}) must be SuperAdmin")
current_app.logger.info("cas_users_import_excel_file by {current_user}")
users_infos = sco_import_users.read_users_excel_file(
datafile, titles=CAS_USER_INFO_IDS
)
return cas_users_import_data(users_infos=users_infos)
def cas_users_import_data(users_infos: list[dict]) -> int:
"""Import informations configuration CAS
users est une liste de dict, on utilise seulement les champs:
- user_name : la clé, l'utilisateur DOIT déjà exister
- cas_id : l'ID CAS a enregistrer.
- cas_allow_login
- cas_allow_scodoc_login
Les éventuels autres champs sont ignorés.
Return: nb de comptes modifiés.
"""
nb_modif = 0
users = []
for info in users_infos:
user: User = User.query.filter_by(user_name=info["user_name"]).first()
if not user:
return False, f"utilisateur {info['user_name']} inexistant", 0
modif = False
if info["cas_id"].strip() != (user.cas_id or ""):
user.cas_id = info["cas_id"].strip() or None
modif = True
val = scu.to_bool(info["cas_allow_login"])
if val != user.cas_allow_login:
user.cas_allow_login = val
modif = True
val = scu.to_bool(info["cas_allow_scodoc_login"])
if val != user.cas_allow_scodoc_login:
user.cas_allow_scodoc_login = val
modif = True
if modif:
nb_modif += 1
# Record modifications
for user in users:
try:
db.session.add(user)
except Exception as exc:
db.session.rollback()
raise ScoValueError(
"Erreur (1) durant l'importation des modifications"
) from exc
try:
db.session.commit()
except Exception as exc:
db.session.rollback()
raise ScoValueError(
"Erreur (2) durant l'importation des modifications"
) from exc
return nb_modif

View File

@ -1,13 +1,12 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""Formulaires authentification """Formulaires authentification
TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentification
""" """
from urllib.parse import urlparse, urljoin from urllib.parse import urlparse, urljoin
from flask import request, url_for, redirect from flask import request, url_for, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import BooleanField, HiddenField, PasswordField, StringField, SubmitField from wtforms import BooleanField, HiddenField, PasswordField, StringField, SubmitField
from wtforms.fields.simple import FileField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.auth.models import User, is_valid_password from app.auth.models import User, is_valid_password
@ -98,3 +97,12 @@ class ResetPasswordForm(FlaskForm):
class DeactivateUserForm(FlaskForm): class DeactivateUserForm(FlaskForm):
submit = SubmitField("Modifier l'utilisateur") submit = SubmitField("Modifier l'utilisateur")
cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True}) cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True})
class CASUsersImportConfigForm(FlaskForm):
user_config_file = FileField(
label="Fichier Excel à réimporter",
description="""fichier avec les paramètres CAS renseignés""",
)
submit = SubmitField("Importer le fichier utilisateurs")
cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True})

View File

@ -21,7 +21,7 @@ import jwt
from app import db, log, login from app import db, log, login
from app.models import Departement from app.models import Departement
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -53,12 +53,12 @@ class User(UserMixin, db.Model):
"""ScoDoc users, handled by Flask / SQLAlchemy""" """ScoDoc users, handled by Flask / SQLAlchemy"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_name = db.Column(db.String(64), index=True, unique=True) user_name = db.Column(db.String(USERNAME_STR_LEN), index=True, unique=True)
"le login" "le login"
email = db.Column(db.String(120)) email = db.Column(db.String(120))
nom = db.Column(db.String(64)) nom = db.Column(db.String(USERNAME_STR_LEN))
prenom = db.Column(db.String(64)) prenom = db.Column(db.String(USERNAME_STR_LEN))
dept = db.Column(db.String(SHORT_STR_LEN), index=True) dept = db.Column(db.String(SHORT_STR_LEN), index=True)
"acronyme du département de l'utilisateur" "acronyme du département de l'utilisateur"
active = db.Column(db.Boolean, default=True, index=True) active = db.Column(db.Boolean, default=True, index=True)

View File

@ -11,17 +11,20 @@ from sqlalchemy import func
from app import db from app import db
from app.auth import bp from app.auth import bp
from app.auth import cas
from app.auth.forms import ( from app.auth.forms import (
CASUsersImportConfigForm,
LoginForm, LoginForm,
UserCreationForm,
ResetPasswordRequestForm,
ResetPasswordForm, ResetPasswordForm,
ResetPasswordRequestForm,
UserCreationForm,
) )
from app.auth.models import Role from app.auth.models import Role
from app.auth.models import User from app.auth.models import User
from app.auth.email import send_password_reset_email from app.auth.email import send_password_reset_email
from app.decorators import admin_required from app.decorators import admin_required
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.scodoc import sco_utils as scu
_ = lambda x: x # sans babel _ = lambda x: x # sans babel
_l = _ _l = _
@ -162,3 +165,34 @@ def reset_standard_roles_permissions():
Role.reset_standard_roles_permissions() Role.reset_standard_roles_permissions()
flash("rôles standards réinitialisés !") flash("rôles standards réinitialisés !")
return redirect(url_for("scodoc.configuration")) return redirect(url_for("scodoc.configuration"))
@bp.route("/cas_users_generate_excel_sample")
@admin_required
def cas_users_generate_excel_sample():
"une feuille excel pour importation config CAS"
data = cas.cas_users_generate_excel_sample()
return scu.send_file(data, "ImportConfigCAS", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
@bp.route("/cas_users_import_config", methods=["GET", "POST"])
@admin_required
def cas_users_import_config():
"""Import utilisateurs depuis feuille Excel"""
form = CASUsersImportConfigForm()
if form.validate_on_submit():
if form.cancel.data: # cancel button
return redirect(url_for("scodoc.configuration"))
datafile = request.files[form.user_config_file.name]
nb_modif = cas.cas_users_import_excel_file(datafile)
current_app.logger.info(f"cas_users_import_config: {nb_modif} comptes modifiés")
flash(f"Config. CAS de {nb_modif} comptes modifiée.")
return redirect(url_for("scodoc.configuration"))
return render_template(
"auth/cas_users_import_config.j2",
title=_("Importation configuration CAS utilisateurs"),
form=form,
)
return

View File

@ -9,6 +9,7 @@ CODE_STR_LEN = 16 # chaine pour les codes
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
GROUPNAME_STR_LEN = 64 GROUPNAME_STR_LEN = 64
USERNAME_STR_LEN = 64
convention = { convention = {
"ix": "ix_%(column_0_label)s", "ix": "ix_%(column_0_label)s",

View File

@ -323,8 +323,8 @@ class ScoExcelSheet:
if not comment is None: if not comment is None:
cell.comment = Comment(comment, "scodoc") cell.comment = Comment(comment, "scodoc")
lines = comment.splitlines() lines = comment.splitlines()
cell.comment.width = 7 * max([len(line) for line in lines]) cell.comment.width = 7 * max([len(line) for line in lines]) if lines else 7
cell.comment.height = 20 * len(lines) cell.comment.height = 20 * len(lines) if lines else 20
# test datatype to overwrite datetime format # test datatype to overwrite datetime format
if isinstance(value, datetime.date): if isinstance(value, datetime.date):
@ -390,9 +390,13 @@ class ScoExcelSheet:
def excel_simple_table( def excel_simple_table(
titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None titles: list[str] = None,
lines: list[list[str]] = None,
sheet_name: str = "feuille",
titles_styles=None,
comments=None,
): ):
"""Export simple type 'CSV': 1ere ligne en gras, le reste tel quel""" """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel."""
ws = ScoExcelSheet(sheet_name) ws = ScoExcelSheet(sheet_name)
if titles is None: if titles is None:
titles = [] titles = []
@ -418,9 +422,9 @@ def excel_simple_table(
cells = [] cells = []
for it in line: for it in line:
cell_style = default_style cell_style = default_style
if type(it) == float: if isinstance(it, float):
cell_style = float_style cell_style = float_style
elif type(it) == int: elif isinstance(it, int):
cell_style = int_style cell_style = int_style
else: else:
cell_style = text_style cell_style = text_style

View File

@ -264,7 +264,7 @@ def scolars_import_excel_file(
"""Importe etudiants depuis fichier Excel """Importe etudiants depuis fichier Excel
et les inscrit dans le semestre indiqué (et à TOUS ses modules) et les inscrit dans le semestre indiqué (et à TOUS ses modules)
""" """
log("scolars_import_excel_file: formsemestre_id=%s" % formsemestre_id) log(f"scolars_import_excel_file: {formsemestre_id}")
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
annee_courante = time.localtime()[0] annee_courante = time.localtime()[0]
@ -287,13 +287,11 @@ def scolars_import_excel_file(
) and tit not in exclude_cols: ) and tit not in exclude_cols:
titles[tit] = l[1:] # title : (Type, Table, AllowNulls, Description) titles[tit] = l[1:] # title : (Type, Table, AllowNulls, Description)
# log("titles=%s" % titles)
# remove quotes, downcase and keep only 1st word # remove quotes, downcase and keep only 1st word
try: try:
fs = [scu.stripquotes(s).lower().split()[0] for s in data[0]] fs = [scu.stripquotes(s).lower().split()[0] for s in data[0]]
except: except Exception as exc:
raise ScoValueError("Titres de colonnes invalides (ou vides ?)") raise ScoValueError("Titres de colonnes invalides (ou vides ?)") from exc
# log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
# check columns titles # check columns titles
if len(fs) != len(titles): if len(fs) != len(titles):

View File

@ -30,7 +30,6 @@
import random import random
import time import time
from email.mime.multipart import MIMEMultipart
from flask import url_for from flask import url_for
from flask_login import current_user from flask_login import current_user
@ -44,7 +43,17 @@ from app.scodoc import sco_excel
from app.scodoc import sco_users from app.scodoc import sco_users
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept", "cas_id") TITLES = (
"user_name",
"nom",
"prenom",
"email",
"roles",
"dept",
"cas_id",
"cas_allow_login",
"cas_allow_scodoc_login",
)
COMMENTS = ( COMMENTS = (
"""user_name: """user_name:
L'identifiant (login). L'identifiant (login).
@ -64,11 +73,17 @@ COMMENTS = (
Exemple: "Ens_RT,Admin_INFO". Exemple: "Ens_RT,Admin_INFO".
""", """,
"""dept: """dept:
Le département d'appartenance de l'utilisateur. Laisser vide si l'utilisateur intervient dans plusieurs départements. Le département d'appartenance de l'utilisateur. Laisser vide si l'utilisateur
intervient dans plusieurs départements.
""", """,
"""cas_id: """cas_id:
identifiant de l'utilisateur sur CAS (optionnel).
Identifiant de l'utilisateur sur CAS (optionnel). """,
"""cas_allow_login:
autorise la connexion via CAS (optionnel, faux par défaut)
""",
"""cas_allow_scodoc_login
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
""", """,
) )
@ -86,62 +101,68 @@ def generate_excel_sample():
) )
def import_excel_file(datafile, force=""): def read_users_excel_file(datafile, titles=TITLES) -> list[dict]:
"""Lit le fichier excel et renvoie liste de dict
titles : titres (ids) des colonnes attendues
""" """
Import scodoc users from Excel file.
This method:
* checks that the current_user has the ability to do so (at the moment only a SuperAdmin). He may thereoff import users with any well formed role into any department (or all)
* Once the check is done ans successfull, build the list of users (does not check the data)
* call :func:`import_users` to actually do the job
history: scodoc7 with no SuperAdmin every Admin_XXX could import users.
:param datafile: the stream from to the to be imported
:return: same as import users
"""
# Check current user privilege
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
# Récupération des informations sur l'utilisateur courant
log("sco_import_users.import_excel_file by %s" % auth_name)
# Read the data from the stream # Read the data from the stream
exceldata = datafile.read() exceldata = datafile.read()
if not exceldata: if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide") raise ScoValueError("Ficher excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata) _, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: if not data:
raise ScoValueError( raise ScoValueError("Le fichier xlsx attendu semble vide !")
"""Le fichier xlsx attendu semble vide !
"""
)
# 1- --- check title line # 1- --- check title line
fs = [scu.stripquotes(s).lower() for s in data[0]] xls_titles = [scu.stripquotes(s).lower() for s in data[0]]
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data))) # log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
# check cols # check cols
cols = {}.fromkeys(TITLES) cols = {}.fromkeys(titles)
unknown = [] unknown = []
for tit in fs: for tit in xls_titles:
if tit not in cols: if tit not in cols:
unknown.append(tit) unknown.append(tit)
else: else:
del cols[tit] del cols[tit]
if cols or unknown: if cols or unknown:
raise ScoValueError( raise ScoValueError(
"""colonnes incorrectes (on attend %d, et non %d) <br> f"""colonnes incorrectes (on attend {len(titles)}, et non {len(xls_titles)})
(colonnes manquantes: %s, colonnes invalides: %s)""" <br>
% (len(TITLES), len(fs), list(cols.keys()), unknown) (colonnes manquantes: {list(cols.keys())}, colonnes invalides: {unknown})
"""
) )
# ok, same titles... : build the list of dictionaries # ok, same titles... : build the list of dictionaries
users = [] users = []
for line in data[1:]: for line in data[1:]:
d = {} d = {}
for i in range(len(fs)): for i, field in enumerate(xls_titles):
d[fs[i]] = line[i] d[field] = line[i]
users.append(d) users.append(d)
return users
def import_excel_file(datafile, force="") -> tuple[bool, list[str], int]:
"""
Import scodoc users from Excel file.
This method:
* checks that the current_user has the ability to do so (at the moment only a SuperAdmin).
He may thereoff import users with any well formed role into any department (or all)
* Once the check is done ans successfull, build the list of users (does not check the data)
* call :func:`import_users` to actually do the job
:param datafile: the stream to be imported
:return: same as import users
"""
if not current_user.is_administrator():
raise AccessDenied(f"invalid user ({current_user}) must be SuperAdmin")
log("sco_import_users.import_excel_file by {current_user}")
users = read_users_excel_file(datafile)
return import_users(users=users, force=force) return import_users(users=users, force=force)
def import_users(users, force=""): def import_users(users, force="") -> tuple[bool, list[str], int]:
""" """
Import users from a list of users_descriptors. Import users from a list of users_descriptors.
@ -157,12 +178,13 @@ def import_users(users, force=""):
Implémentation: Implémentation:
Pour chaque utilisateur à créer: Pour chaque utilisateur à créer:
* vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois) * vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé
plusieurs fois)
* générer mot de passe aléatoire * générer mot de passe aléatoire
* créer utilisateur et mettre le mot de passe * créer utilisateur et mettre le mot de passe
* envoyer mot de passe par mail * envoyer mot de passe par mail
Les utilisateurs à créer sont stockés dans un dictionnaire. Les utilisateurs à créer sont stockés dans un dictionnaire.
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée.
""" """
created = {} # uid créés created = {} # uid créés
@ -175,7 +197,7 @@ def import_users(users, force=""):
import_ok = True import_ok = True
def append_msg(msg): def append_msg(msg):
msg_list.append("Ligne %s : %s" % (line, msg)) msg_list.append(f"Ligne {line} : {msg}")
try: try:
for u in users: for u in users:
@ -189,9 +211,10 @@ def import_users(users, force=""):
email=u["email"], email=u["email"],
roles=[r for r in u["roles"].split(",") if r], roles=[r for r in u["roles"].split(",") if r],
dept=u["dept"], dept=u["dept"],
cas_id=u["cas_id"],
) )
if not user_ok: if not user_ok:
append_msg("identifiant '%s' %s" % (u["user_name"], msg)) append_msg(f"""identifiant '{u["user_name"]}' {msg}""")
u["passwd"] = generate_password() u["passwd"] = generate_password()
# #
@ -199,8 +222,8 @@ def import_users(users, force=""):
if u["user_name"] in created.keys(): if u["user_name"] in created.keys():
user_ok = False user_ok = False
append_msg( append_msg(
"l'utilisateur '%s' a déjà été décrit ligne %s" f"""l'utilisateur '{u["user_name"]}' a déjà été décrit ligne {
% (u["user_name"], created[u["user_name"]]["line"]) created[u["user_name"]]["line"]}"""
) )
# check roles / ignore whitespaces around roles / build roles_string # check roles / ignore whitespaces around roles / build roles_string
# roles_string (expected by User) appears as column 'roles' in excel file # roles_string (expected by User) appears as column 'roles' in excel file
@ -213,7 +236,7 @@ def import_users(users, force=""):
roles_list.append(role) roles_list.append(role)
except ScoValueError as value_error: except ScoValueError as value_error:
user_ok = False user_ok = False
append_msg("role %s : %s" % (role, value_error)) append_msg(f"role {role} : {value_error}")
u["roles_string"] = ",".join(roles_list) u["roles_string"] = ",".join(roles_list)
if user_ok: if user_ok:
u["line"] = line u["line"] = line
@ -307,7 +330,7 @@ Pour plus d'informations sur ce logiciel, voir %s
""" """
% scu.SCO_WEBSITE % scu.SCO_WEBSITE
) )
msg = MIMEMultipart()
if reset: if reset:
subject = "Mot de passe ScoDoc" subject = "Mot de passe ScoDoc"
else: else:

View File

@ -37,9 +37,8 @@ from flask_login import current_user
from app import db, Departement from app import db, Departement
from app.auth.models import Permission from app.auth.models import Permission, User
from app.auth.models import User from app.models import ScoDocSiteConfig, USERNAME_STR_LEN
from app.models import ScoDocSiteConfig
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
@ -270,20 +269,24 @@ MSG_OPT = """<br>Attention: (vous pouvez forcer l'opération en cochant "<em>Ign
def check_modif_user( def check_modif_user(
edit, edit: bool,
enforce_optionals=False, enforce_optionals: bool = False,
user_name="", user_name: str = "",
nom="", nom: str = "",
prenom="", prenom: str = "",
email="", email: str = "",
dept="", dept: str = "",
roles: list = None, roles: list = None,
cas_id: str = None, cas_id: str = None,
): ) -> tuple[bool, str]:
"""Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1) """Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1)
Cherche homonymes. Cherche homonymes.
Ne vérifie PAS que l'on a la permission de faire la modif. Ne vérifie PAS que l'on a la permission de faire la modif.
returns (ok, msg)
edit: si vrai, mode "edition" (modif d'un objet existant)
enforce_optionals: vérifie que les champs optionnels sont cohérents.
Returns (ok, msg)
- ok : si vrai, peut continuer avec ces parametres - ok : si vrai, peut continuer avec ces parametres
(si ok est faux, l'utilisateur peut quand même forcer la creation) (si ok est faux, l'utilisateur peut quand même forcer la creation)
- msg: message warning à presenter à l'utilisateur - msg: message warning à presenter à l'utilisateur
@ -302,12 +305,18 @@ def check_modif_user(
False, False,
f"identifiant '{user_name}' invalide (pas d'accents ni de caractères spéciaux)", f"identifiant '{user_name}' invalide (pas d'accents ni de caractères spéciaux)",
) )
if enforce_optionals and len(user_name) > 64: if len(user_name) > USERNAME_STR_LEN:
return False, f"identifiant '{user_name}' trop long (64 caractères)" return (
if enforce_optionals and len(nom) > 64: False,
return False, f"nom '{nom}' trop long (64 caractères)" + MSG_OPT f"identifiant '{user_name}' trop long ({USERNAME_STR_LEN} caractères)",
if enforce_optionals and len(prenom) > 64: )
return False, f"prenom '{prenom}' trop long (64 caractères)" + MSG_OPT if len(nom) > USERNAME_STR_LEN:
return False, f"nom '{nom}' trop long ({USERNAME_STR_LEN} caractères)" + MSG_OPT
if len(prenom) > 64:
return (
False,
f"prenom '{prenom}' trop long ({USERNAME_STR_LEN} caractères)" + MSG_OPT,
)
# check that same user_name has not already been described in this import # check that same user_name has not already been described in this import
if not email: if not email:
return False, "vous devriez indiquer le mail de l'utilisateur créé !" return False, "vous devriez indiquer le mail de l'utilisateur créé !"

View File

@ -637,15 +637,24 @@ def is_valid_filename(filename):
BOOL_STR = { BOOL_STR = {
"": False, "": False,
"false": False,
"0": False, "0": False,
"1": True, "1": True,
"f": False,
"false": False,
"n": False,
"t": True,
"true": True, "true": True,
"y": True,
} }
def to_bool(x) -> bool: def to_bool(x) -> bool:
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """ """Cast value to boolean.
The value may be encoded as a string
False are: empty, "0", "False", "f", "n".
True: all other values, such as "1", "True", "foo", "bar"...
Case insentive, ignore leading and trailing spaces.
"""
if isinstance(x, str): if isinstance(x, str):
return BOOL_STR.get(x.lower().strip(), True) return BOOL_STR.get(x.lower().strip(), True)
return bool(x) return bool(x)

View File

@ -0,0 +1,47 @@
{% extends "base.j2" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Chargement des configurations CAS des utilisateurs</h1>
<div class="help" style="max-width: 600px;">
<p style="color: red">A utiliser pour modifier le paramétrage CAS de
<b>comptes utilisateurs existants</b>
</p>
<p>L'opération se déroule en plusieurs étapes:
</p>
<ol>
<li> Dans un premier temps, vous téléchargez une feuille Excel pré-remplie
avec la liste des tous les utilisateurs.
</li>
<li>Vous modifiez cette feuille avec votre logiciel préféré.
Vous pouvez supprimer des lignes, mais pas en ajouter.
<br>
Il faut remplir ou modifier le contenu des colonnes <tt>cas_id</tt>,
<tt>cas_allow_login</tt> et <tt>cas_allow_scodoc_login</tt>.
<br>
Les autres colonnes sont là pour information et seront ignorées à l'import,
sauf évidemment <tt>user_name</tt> qui sert à repérer l'utilisateur.
</li>
<li>Revenez sur cette page et chargez le fichier dans ScoDoc.
</li>
</ol>
<ul>
<li><b>Étape 1: </b><a class="stdlink" href="{{
url_for('auth.cas_users_generate_excel_sample')
}}">Obtenir la feuille excel à remplir</a>,
avec la liste complète des utilisateurs.
</li>
<li style="margin-top: 8px;"><b>Étape 2:</b>
<div class="row">
<div class="col-md-8">
{{ wtf.quick_form(form) }}
</div>
</div>
</li>
</ul>
{% endblock %}

View File

@ -19,11 +19,11 @@
{% endif %} {% endif %}
</div> </div>
<div style="margin-top:16px;"> <div style="margin-top:16px;">
<em>Note: si le CAS est forcé, le super-admin et les utilisateurs autorisés <em>Note: si le CAS est forcé, le super-admin et les utilisateurs autorisés
à "se connecter via ScoDoc" pourront toujours se à "se connecter via ScoDoc" pourront toujours se
connecter via l'adresse spéciale</em> connecter via l'adresse spéciale</em>
<tt style="color: blue;">{{url_for("auth.login_scodoc", _external=True)}}</tt> <tt style="color: blue;">{{url_for("auth.login_scodoc", _external=True)}}</tt>
</div> </div>
</div> </div>
</div> </div>

View File

@ -55,10 +55,18 @@
<h2>Utilisateurs et CAS</h2> <h2>Utilisateurs et CAS</h2>
<section> <section>
<p><a class="stdlink" href="{{url_for('scodoc.config_cas')}}">configuration du service CAS</a> <div>
<p><a class="stdlink" href="{{url_for('auth.reset_standard_roles_permissions')}}">remettre 🏰 <a class="stdlink" href="{{url_for('scodoc.config_cas')}}">Configuration du service CAS</a>
les permissions des rôles standards à leurs valeurs par défaut</a> (efface les modifications apportées) </div>
</p> <div style="margin-top: 16px;">
🧑🏾‍🤝‍🧑🏼 <a class="stdlink" href="{{ url_for('auth.cas_users_import_config') }}">
Configurer les comptes utilisateurs pour le CAS</a>
</div>
<div style="margin-top: 16px;">
🛟 <a class="stdlink" href="{{url_for('auth.reset_standard_roles_permissions')}}">Remettre
les permissions des rôles standards à leurs valeurs par défaut</a>
(efface les modifications apportées aux rôles)
</div>
</section> </section>
<h2>ScoDoc</h2> <h2>ScoDoc</h2>

View File

@ -1938,13 +1938,10 @@ def form_students_import_excel(formsemestre_id=None):
formsemestre_id = int(formsemestre_id) if formsemestre_id else None formsemestre_id = int(formsemestre_id) if formsemestre_id else None
if formsemestre_id: if formsemestre_id:
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
dest_url = ( dest_url = url_for(
# scu.ScoURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id # TODO: Remplacer par for_url ? "notes.formsemestre_status",
url_for( scodoc_dept=g.scodoc_dept,
"notes.formsemestre_status", formsemestre_id=formsemestre_id,
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
else: else:
sem = None sem = None
@ -2091,7 +2088,6 @@ def import_generate_excel_sample(with_codesemestre="1"):
return scu.send_file( return scu.send_file(
data, "ImportEtudiants", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE data, "ImportEtudiants", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
) )
# return sco_excel.send_excel_file(data, "ImportEtudiants" + scu.XLSX_SUFFIX)
# --- Données admission # --- Données admission

View File

@ -758,8 +758,8 @@ def import_users_form():
head = html_sco_header.sco_header(page_title="Import utilisateurs") head = html_sco_header.sco_header(page_title="Import utilisateurs")
H = [ H = [
head, head,
"""<h2>Téléchargement d'une nouvelle liste d'utilisateurs</h2> """<h2>Téléchargement d'une liste d'utilisateurs</h2>
<p style="color: red">A utiliser pour importer de <b>nouveaux</b> <p style="color: red">A utiliser pour importer de <b>nouveaux</b>
utilisateurs (enseignants ou secrétaires) utilisateurs (enseignants ou secrétaires)
</p> </p>
<p> <p>
@ -782,9 +782,14 @@ def import_users_form():
<li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li> <li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li>
</ol>""" </ol>"""
H.append( H.append(
f"""<ol><li><a class="stdlink" href="{ f"""<ul>
<li><b>Étape 1: </b><a class="stdlink" href="{
url_for("users.import_users_generate_excel_sample", scodoc_dept=g.scodoc_dept) url_for("users.import_users_generate_excel_sample", scodoc_dept=g.scodoc_dept)
}">Obtenir la feuille excel à remplir</a></li><li>""" }">Obtenir la feuille excel vide à remplir</a>
ou bien la liste complète des utilisateurs.
</li>
<li><b> Étape 2:</b>
"""
) )
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
tf = TrivialFormulator( tf = TrivialFormulator(
@ -810,7 +815,7 @@ def import_users_form():
submitlabel="Télécharger", submitlabel="Télécharger",
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + help_msg + F return "\n".join(H) + tf[1] + "</li></ul>" + help_msg + F
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept)) return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept))