diff --git a/app/auth/cas.py b/app/auth/cas.py index f47641f19d..2f231d2adb 100644 --- a/app/auth/cas.py +++ b/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 diff --git a/app/auth/forms.py b/app/auth/forms.py index bfb8676b5a..13d212ee49 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -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}) diff --git a/app/auth/models.py b/app/auth/models.py index 310589afb2..18dcddbfd5 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -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) diff --git a/app/auth/routes.py b/app/auth/routes.py index ab06dfb16b..eb279acb58 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -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 diff --git a/app/models/__init__.py b/app/models/__init__.py index a832ad74f0..9402e1ee1d 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -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", diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 553579ea5a..2f2c93b558 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -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 diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 7f52e9d253..110d95d889 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -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): diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py index e593b4eccd..550e0fdbed 100644 --- a/app/scodoc/sco_import_users.py +++ b/app/scodoc/sco_import_users.py @@ -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)
- (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)}) +
+ (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: diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index 16647f9aa0..d44fbbce47 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -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 = """
Attention: (vous pouvez forcer l'opération en cochant "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éé !" diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index dc4fb9772a..d56466c3eb 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -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) diff --git a/app/templates/auth/cas_users_import_config.j2 b/app/templates/auth/cas_users_import_config.j2 new file mode 100644 index 0000000000..4bcd8e0d88 --- /dev/null +++ b/app/templates/auth/cas_users_import_config.j2 @@ -0,0 +1,47 @@ +{% extends "base.j2" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

Chargement des configurations CAS des utilisateurs

+ +
+

A utiliser pour modifier le paramétrage CAS de + comptes utilisateurs existants +

+

L'opération se déroule en plusieurs étapes: +

+
    +
  1. Dans un premier temps, vous téléchargez une feuille Excel pré-remplie + avec la liste des tous les utilisateurs. +
  2. +
  3. Vous modifiez cette feuille avec votre logiciel préféré. + Vous pouvez supprimer des lignes, mais pas en ajouter. +
    + Il faut remplir ou modifier le contenu des colonnes cas_id, + cas_allow_login et cas_allow_scodoc_login. +
    + Les autres colonnes sont là pour information et seront ignorées à l'import, + sauf évidemment user_name qui sert à repérer l'utilisateur. +
  4. + +
  5. Revenez sur cette page et chargez le fichier dans ScoDoc. +
  6. +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/config_cas.j2 b/app/templates/config_cas.j2 index 64e4f3c1a3..5c0cebd15f 100644 --- a/app/templates/config_cas.j2 +++ b/app/templates/config_cas.j2 @@ -19,11 +19,11 @@ {% endif %}
-ℹ️ 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 - {{url_for("auth.login_scodoc", _external=True)}} -
+ ℹ️ 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 + {{url_for("auth.login_scodoc", _external=True)}} + diff --git a/app/templates/configuration.j2 b/app/templates/configuration.j2 index 9113e3c363..d011e0911a 100644 --- a/app/templates/configuration.j2 +++ b/app/templates/configuration.j2 @@ -55,10 +55,18 @@

Utilisateurs et CAS

-

configuration du service CAS -

remettre - les permissions des rôles standards à leurs valeurs par défaut (efface les modifications apportées) -

+
+ 🏰 Configuration du service CAS +
+
+ 🧑🏾‍🤝‍🧑🏼 + Configurer les comptes utilisateurs pour le CAS +
+
+ 🛟 Remettre + les permissions des rôles standards à leurs valeurs par défaut + (efface les modifications apportées aux rôles) +

ScoDoc

diff --git a/app/views/scolar.py b/app/views/scolar.py index d1e8ebea18..03e158c4c0 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -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 diff --git a/app/views/users.py b/app/views/users.py index c4b9f93219..4d70b31a27 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -758,8 +758,8 @@ def import_users_form(): head = html_sco_header.sco_header(page_title="Import utilisateurs") H = [ head, - """

Téléchargement d'une nouvelle liste d'utilisateurs

-

A utiliser pour importer de nouveaux + """

Téléchargement d'une liste d'utilisateurs

+

A utiliser pour importer de nouveaux utilisateurs (enseignants ou secrétaires)

@@ -782,9 +782,14 @@ def import_users_form():

  • envoi à chaque utilisateur de son mot de passe initial par mail.
  • """ H.append( - f"""
    1. +
    2. Étape 1: Obtenir la feuille excel à remplir
    3. """ + }">Obtenir la feuille excel vide à remplir + ou bien la liste complète des utilisateurs. +
    4. +
    5. Étape 2: + """ ) 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] + "
    " + help_msg + F + return "\n".join(H) + tf[1] + "" + help_msg + F elif tf[0] == -1: return flask.redirect(url_for("scolar.index_html", docodc_dept=g.scodoc_dept))