forked from ScoDoc/ScoDoc
CAS: export/import config comptes via Excel
This commit is contained in:
parent
0c0d43d075
commit
3edf34dfee
136
app/auth/cas.py
136
app/auth/cas.py
@ -6,13 +6,15 @@ import datetime
|
||||
|
||||
import flask
|
||||
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.auth import bp
|
||||
from app.auth.models import User
|
||||
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.
|
||||
|
||||
@ -94,3 +96,133 @@ def set_cas_configuration(app: flask.app.Flask = None):
|
||||
app.config.pop("CAS_AFTER_LOGOUT", None)
|
||||
app.config.pop("CAS_SSL_VERIFY", 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
|
||||
|
@ -1,13 +1,12 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Formulaires authentification
|
||||
|
||||
TODO: à revoir complètement pour reprendre ZScoUsers et les pages d'authentification
|
||||
"""
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from flask import request, url_for, redirect
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, HiddenField, PasswordField, StringField, SubmitField
|
||||
from wtforms.fields.simple import FileField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||
from app.auth.models import User, is_valid_password
|
||||
|
||||
@ -98,3 +97,12 @@ class ResetPasswordForm(FlaskForm):
|
||||
class DeactivateUserForm(FlaskForm):
|
||||
submit = SubmitField("Modifier l'utilisateur")
|
||||
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})
|
||||
|
@ -21,7 +21,7 @@ import jwt
|
||||
|
||||
from app import db, log, login
|
||||
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.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -53,12 +53,12 @@ class User(UserMixin, db.Model):
|
||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||
|
||||
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"
|
||||
email = db.Column(db.String(120))
|
||||
|
||||
nom = db.Column(db.String(64))
|
||||
prenom = db.Column(db.String(64))
|
||||
nom = db.Column(db.String(USERNAME_STR_LEN))
|
||||
prenom = db.Column(db.String(USERNAME_STR_LEN))
|
||||
dept = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||
"acronyme du département de l'utilisateur"
|
||||
active = db.Column(db.Boolean, default=True, index=True)
|
||||
|
@ -11,17 +11,20 @@ from sqlalchemy import func
|
||||
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.auth import cas
|
||||
from app.auth.forms import (
|
||||
CASUsersImportConfigForm,
|
||||
LoginForm,
|
||||
UserCreationForm,
|
||||
ResetPasswordRequestForm,
|
||||
ResetPasswordForm,
|
||||
ResetPasswordRequestForm,
|
||||
UserCreationForm,
|
||||
)
|
||||
from app.auth.models import Role
|
||||
from app.auth.models import User
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
_ = lambda x: x # sans babel
|
||||
_l = _
|
||||
@ -162,3 +165,34 @@ def reset_standard_roles_permissions():
|
||||
Role.reset_standard_roles_permissions()
|
||||
flash("rôles standards réinitialisés !")
|
||||
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
|
||||
|
@ -9,6 +9,7 @@ CODE_STR_LEN = 16 # chaine pour les codes
|
||||
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)
|
||||
GROUPNAME_STR_LEN = 64
|
||||
USERNAME_STR_LEN = 64
|
||||
|
||||
convention = {
|
||||
"ix": "ix_%(column_0_label)s",
|
||||
|
@ -323,8 +323,8 @@ class ScoExcelSheet:
|
||||
if not comment is None:
|
||||
cell.comment = Comment(comment, "scodoc")
|
||||
lines = comment.splitlines()
|
||||
cell.comment.width = 7 * max([len(line) for line in lines])
|
||||
cell.comment.height = 20 * len(lines)
|
||||
cell.comment.width = 7 * max([len(line) for line in lines]) if lines else 7
|
||||
cell.comment.height = 20 * len(lines) if lines else 20
|
||||
|
||||
# test datatype to overwrite datetime format
|
||||
if isinstance(value, datetime.date):
|
||||
@ -390,9 +390,13 @@ class ScoExcelSheet:
|
||||
|
||||
|
||||
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)
|
||||
if titles is None:
|
||||
titles = []
|
||||
@ -418,9 +422,9 @@ def excel_simple_table(
|
||||
cells = []
|
||||
for it in line:
|
||||
cell_style = default_style
|
||||
if type(it) == float:
|
||||
if isinstance(it, float):
|
||||
cell_style = float_style
|
||||
elif type(it) == int:
|
||||
elif isinstance(it, int):
|
||||
cell_style = int_style
|
||||
else:
|
||||
cell_style = text_style
|
||||
|
@ -264,7 +264,7 @@ def scolars_import_excel_file(
|
||||
"""Importe etudiants depuis fichier Excel
|
||||
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()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
annee_courante = time.localtime()[0]
|
||||
@ -287,13 +287,11 @@ def scolars_import_excel_file(
|
||||
) and tit not in exclude_cols:
|
||||
titles[tit] = l[1:] # title : (Type, Table, AllowNulls, Description)
|
||||
|
||||
# log("titles=%s" % titles)
|
||||
# remove quotes, downcase and keep only 1st word
|
||||
try:
|
||||
fs = [scu.stripquotes(s).lower().split()[0] for s in data[0]]
|
||||
except:
|
||||
raise ScoValueError("Titres de colonnes invalides (ou vides ?)")
|
||||
# log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
|
||||
except Exception as exc:
|
||||
raise ScoValueError("Titres de colonnes invalides (ou vides ?)") from exc
|
||||
|
||||
# check columns titles
|
||||
if len(fs) != len(titles):
|
||||
|
@ -30,7 +30,6 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
|
||||
@ -44,7 +43,17 @@ from app.scodoc import sco_excel
|
||||
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 = (
|
||||
"""user_name:
|
||||
L'identifiant (login).
|
||||
@ -64,11 +73,17 @@ COMMENTS = (
|
||||
Exemple: "Ens_RT,Admin_INFO".
|
||||
""",
|
||||
"""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:
|
||||
|
||||
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
|
||||
exceldata = datafile.read()
|
||||
if not exceldata:
|
||||
raise ScoValueError("Ficher excel vide ou invalide")
|
||||
_, data = sco_excel.excel_bytes_to_list(exceldata)
|
||||
if not data:
|
||||
raise ScoValueError(
|
||||
"""Le fichier xlsx attendu semble vide !
|
||||
"""
|
||||
)
|
||||
raise ScoValueError("Le fichier xlsx attendu semble vide !")
|
||||
# 1- --- check title line
|
||||
fs = [scu.stripquotes(s).lower() for s in data[0]]
|
||||
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
|
||||
xls_titles = [scu.stripquotes(s).lower() for s in data[0]]
|
||||
# log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
|
||||
# check cols
|
||||
cols = {}.fromkeys(TITLES)
|
||||
cols = {}.fromkeys(titles)
|
||||
unknown = []
|
||||
for tit in fs:
|
||||
for tit in xls_titles:
|
||||
if tit not in cols:
|
||||
unknown.append(tit)
|
||||
else:
|
||||
del cols[tit]
|
||||
if cols or unknown:
|
||||
raise ScoValueError(
|
||||
"""colonnes incorrectes (on attend %d, et non %d) <br>
|
||||
(colonnes manquantes: %s, colonnes invalides: %s)"""
|
||||
% (len(TITLES), len(fs), list(cols.keys()), unknown)
|
||||
f"""colonnes incorrectes (on attend {len(titles)}, et non {len(xls_titles)})
|
||||
<br>
|
||||
(colonnes manquantes: {list(cols.keys())}, colonnes invalides: {unknown})
|
||||
"""
|
||||
)
|
||||
# ok, same titles... : build the list of dictionaries
|
||||
users = []
|
||||
for line in data[1:]:
|
||||
d = {}
|
||||
for i in range(len(fs)):
|
||||
d[fs[i]] = line[i]
|
||||
for i, field in enumerate(xls_titles):
|
||||
d[field] = line[i]
|
||||
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)
|
||||
|
||||
|
||||
def import_users(users, force=""):
|
||||
def import_users(users, force="") -> tuple[bool, list[str], int]:
|
||||
"""
|
||||
Import users from a list of users_descriptors.
|
||||
|
||||
@ -157,12 +178,13 @@ def import_users(users, force=""):
|
||||
|
||||
Implémentation:
|
||||
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
|
||||
* créer utilisateur et mettre le mot de passe
|
||||
* envoyer mot de passe par mail
|
||||
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
|
||||
@ -175,7 +197,7 @@ def import_users(users, force=""):
|
||||
import_ok = True
|
||||
|
||||
def append_msg(msg):
|
||||
msg_list.append("Ligne %s : %s" % (line, msg))
|
||||
msg_list.append(f"Ligne {line} : {msg}")
|
||||
|
||||
try:
|
||||
for u in users:
|
||||
@ -189,9 +211,10 @@ def import_users(users, force=""):
|
||||
email=u["email"],
|
||||
roles=[r for r in u["roles"].split(",") if r],
|
||||
dept=u["dept"],
|
||||
cas_id=u["cas_id"],
|
||||
)
|
||||
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()
|
||||
#
|
||||
@ -199,8 +222,8 @@ def import_users(users, force=""):
|
||||
if u["user_name"] in created.keys():
|
||||
user_ok = False
|
||||
append_msg(
|
||||
"l'utilisateur '%s' a déjà été décrit ligne %s"
|
||||
% (u["user_name"], created[u["user_name"]]["line"])
|
||||
f"""l'utilisateur '{u["user_name"]}' a déjà été décrit ligne {
|
||||
created[u["user_name"]]["line"]}"""
|
||||
)
|
||||
# check roles / ignore whitespaces around roles / build roles_string
|
||||
# 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)
|
||||
except ScoValueError as value_error:
|
||||
user_ok = False
|
||||
append_msg("role %s : %s" % (role, value_error))
|
||||
append_msg(f"role {role} : {value_error}")
|
||||
u["roles_string"] = ",".join(roles_list)
|
||||
if user_ok:
|
||||
u["line"] = line
|
||||
@ -307,7 +330,7 @@ Pour plus d'informations sur ce logiciel, voir %s
|
||||
"""
|
||||
% scu.SCO_WEBSITE
|
||||
)
|
||||
msg = MIMEMultipart()
|
||||
|
||||
if reset:
|
||||
subject = "Mot de passe ScoDoc"
|
||||
else:
|
||||
|
@ -37,9 +37,8 @@ from flask_login import current_user
|
||||
|
||||
from app import db, Departement
|
||||
|
||||
from app.auth.models import Permission
|
||||
from app.auth.models import User
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.auth.models import Permission, User
|
||||
from app.models import ScoDocSiteConfig, USERNAME_STR_LEN
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
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(
|
||||
edit,
|
||||
enforce_optionals=False,
|
||||
user_name="",
|
||||
nom="",
|
||||
prenom="",
|
||||
email="",
|
||||
dept="",
|
||||
edit: bool,
|
||||
enforce_optionals: bool = False,
|
||||
user_name: str = "",
|
||||
nom: str = "",
|
||||
prenom: str = "",
|
||||
email: str = "",
|
||||
dept: str = "",
|
||||
roles: list = None,
|
||||
cas_id: str = None,
|
||||
):
|
||||
) -> tuple[bool, str]:
|
||||
"""Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1)
|
||||
Cherche homonymes.
|
||||
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
|
||||
(si ok est faux, l'utilisateur peut quand même forcer la creation)
|
||||
- msg: message warning à presenter à l'utilisateur
|
||||
@ -302,12 +305,18 @@ def check_modif_user(
|
||||
False,
|
||||
f"identifiant '{user_name}' invalide (pas d'accents ni de caractères spéciaux)",
|
||||
)
|
||||
if enforce_optionals and len(user_name) > 64:
|
||||
return False, f"identifiant '{user_name}' trop long (64 caractères)"
|
||||
if enforce_optionals and len(nom) > 64:
|
||||
return False, f"nom '{nom}' trop long (64 caractères)" + MSG_OPT
|
||||
if enforce_optionals and len(prenom) > 64:
|
||||
return False, f"prenom '{prenom}' trop long (64 caractères)" + MSG_OPT
|
||||
if len(user_name) > USERNAME_STR_LEN:
|
||||
return (
|
||||
False,
|
||||
f"identifiant '{user_name}' trop long ({USERNAME_STR_LEN} caractères)",
|
||||
)
|
||||
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
|
||||
if not email:
|
||||
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
|
||||
|
@ -637,15 +637,24 @@ def is_valid_filename(filename):
|
||||
|
||||
BOOL_STR = {
|
||||
"": False,
|
||||
"false": False,
|
||||
"0": False,
|
||||
"1": True,
|
||||
"f": False,
|
||||
"false": False,
|
||||
"n": False,
|
||||
"t": True,
|
||||
"true": True,
|
||||
"y": True,
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
return BOOL_STR.get(x.lower().strip(), True)
|
||||
return bool(x)
|
||||
|
47
app/templates/auth/cas_users_import_config.j2
Normal file
47
app/templates/auth/cas_users_import_config.j2
Normal 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 %}
|
@ -19,11 +19,11 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="margin-top:16px;">
|
||||
ℹ️ <em>Note: si le CAS est forcé, le super-admin et les utilisateurs autorisés
|
||||
à "se connecter via ScoDoc" pourront toujours se
|
||||
connecter via l'adresse spéciale</em>
|
||||
<tt style="color: blue;">{{url_for("auth.login_scodoc", _external=True)}}</tt>
|
||||
</div>
|
||||
ℹ️ <em>Note: si le CAS est forcé, le super-admin et les utilisateurs autorisés
|
||||
à "se connecter via ScoDoc" pourront toujours se
|
||||
connecter via l'adresse spéciale</em>
|
||||
<tt style="color: blue;">{{url_for("auth.login_scodoc", _external=True)}}</tt>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -55,10 +55,18 @@
|
||||
|
||||
<h2>Utilisateurs et CAS</h2>
|
||||
<section>
|
||||
<p><a class="stdlink" href="{{url_for('scodoc.config_cas')}}">configuration du service CAS</a>
|
||||
<p><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)
|
||||
</p>
|
||||
<div>
|
||||
🏰 <a class="stdlink" href="{{url_for('scodoc.config_cas')}}">Configuration du service CAS</a>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<h2>ScoDoc</h2>
|
||||
|
@ -1938,13 +1938,10 @@ def form_students_import_excel(formsemestre_id=None):
|
||||
formsemestre_id = int(formsemestre_id) if formsemestre_id else None
|
||||
if formsemestre_id:
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
dest_url = (
|
||||
# scu.ScoURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id # TODO: Remplacer par for_url ?
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
dest_url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
else:
|
||||
sem = None
|
||||
@ -2091,7 +2088,6 @@ def import_generate_excel_sample(with_codesemestre="1"):
|
||||
return scu.send_file(
|
||||
data, "ImportEtudiants", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
|
||||
)
|
||||
# return sco_excel.send_excel_file(data, "ImportEtudiants" + scu.XLSX_SUFFIX)
|
||||
|
||||
|
||||
# --- Données admission
|
||||
|
@ -758,7 +758,7 @@ def import_users_form():
|
||||
head = html_sco_header.sco_header(page_title="Import utilisateurs")
|
||||
H = [
|
||||
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>
|
||||
utilisateurs (enseignants ou secrétaires)
|
||||
</p>
|
||||
@ -782,9 +782,14 @@ def import_users_form():
|
||||
<li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li>
|
||||
</ol>"""
|
||||
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)
|
||||
}">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()
|
||||
tf = TrivialFormulator(
|
||||
@ -810,7 +815,7 @@ def import_users_form():
|
||||
submitlabel="Télécharger",
|
||||
)
|
||||
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:
|
||||
return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user