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
|
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
|
||||||
|
@ -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})
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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éé !"
|
||||||
|
@ -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)
|
||||||
|
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 %}
|
{% 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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -758,7 +758,7 @@ 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>
|
||||||
@ -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))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user