# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Import d'utilisateurs via fichier Excel """ import random, time import re from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header from app import db, Departement from app.scodoc import sco_emails import app.scodoc.sco_utils as scu from app.scodoc.notes_log import log from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoException from app.scodoc import sco_excel from app.scodoc import sco_preferences from app.scodoc import sco_users from flask import g from flask_login import current_user from app.auth.models import User, UserRole TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept") COMMENTS = ( """user_name: Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _ """, """nom: Maximum 64 caractères""", """prenom: Maximum 64 caractères""", """email: Maximum 120 caractères""", """roles: un plusieurs rôles séparés par ',' chaque role est fait de 2 composantes séparées par _: 1. Le role (Ens, Secr ou Admin) 2. Le département (en majuscule) Exemple: "Ens_RT,Admin_INFO" """, """dept: Le département d'appartenance du l'utillsateur. Laisser vide si l'utilisateur intervient dans plusieurs dépatements """, ) def generate_excel_sample(): """generates an excel document suitable to import users""" style = sco_excel.excel_make_style(bold=True) titles = TITLES titles_styles = [style] * len(titles) return sco_excel.excel_simple_table( titles=titles, titles_styles=titles_styles, sheet_name="Utilisateurs ScoDoc", comments=COMMENTS, ) def import_excel_file(datafile): """ 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 deprtment (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_dept = current_user.dept 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: # probably a bug raise ScoException("import_excel_file: empty file !") # 1- --- check title line fs = [scu.stripquotes(s).lower() for s in data[0]] log("excel: fs='%s'\ndata=%s" % (str(fs), str(data))) # check cols cols = {}.fromkeys(TITLES) unknown = [] for tit in fs: 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) ) # 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] users.append(d) return import_users(users) def import_users(users): """ Import users from a list of users_descriptors. descriptors are dictionaries hosting users's data. The operation is atomic (all the users are imported or none) :param users: list of descriptors to be imported :return: a tuple that describe the result of the import: * ok: import ok or aborted * messages: the list of messages * the # of users created """ """ 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) * 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 """ if len(users) == 0: import_ok = False msg_list = ["Feuille vide ou illisible"] else: created = {} # liste de uid créés msg_list = [] line = 1 # start from excel line #2 import_ok = True def append_msg(msg): msg_list.append("Ligne %s : %s" % (line, msg)) try: for u in users: line = line + 1 user_ok, msg = sco_users.check_modif_user( 0, user_name=u["user_name"], nom=u["nom"], prenom=u["prenom"], email=u["email"], roles=u["roles"].split(","), ) if not user_ok: append_msg("identifiant '%s' %s" % (u["user_name"], msg)) # raise ScoValueError( # "données invalides pour %s: %s" % (u["user_name"], msg) # ) u["passwd"] = generate_password() # # check identifiant if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", u["user_name"]): user_ok = False append_msg( "identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)" % u["user_name"] ) if len(u["user_name"]) > 64: user_ok = False append_msg( "identifiant '%s' trop long (64 caractères)" % u["user_name"] ) if len(u["nom"]) > 64: user_ok = False append_msg("nom '%s' trop long (64 caractères)" % u["nom"]) if len(u["prenom"]) > 64: user_ok = False append_msg("prenom '%s' trop long (64 caractères)" % u["prenom"]) if len(u["email"]) > 120: user_ok = False append_msg("email '%s' trop long (120 caractères)" % u["email"]) # check that tha same user_name has not already been described in this import 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"]) ) # check département if u["dept"] != "": dept = Departement.query.filter_by(acronym=u["dept"]).first() if dept is None: user_ok = False append_msg("département '%s' inexistant" % u["dept"]) # check roles / ignore whitespaces around roles / build roles_string # roles_string (expected by User) appears as column 'roles' in excel file roles_list = [] for role in u["roles"].split(","): try: _, _ = UserRole.role_dept_from_string(role.strip()) roles_list.append(role.strip()) except ScoValueError as value_error: user_ok = False append_msg("role %s : %s" % (role, value_error)) u["roles_string"] = ",".join(roles_list) if user_ok: u["line"] = line created[u["user_name"]] = u else: import_ok = False except ScoValueError as value_error: log("import_users: exception: abort create %s" % str(created.keys())) raise ScoValueError(msg) # re-raise exception if import_ok: for u in created.values(): # Création de l'utilisateur (via SQLAlchemy) user = User() user.from_dict(u, new_user=True) db.session.add(user) db.session.commit() mail_password(u) else: created = [] # reset # of created users to 0 return import_ok, msg_list, len(created) # --------- Génération du mot de passe initial ----------- # Adapté de http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440564 # Alphabet tres simple pour des mots de passe simples... ALPHABET = r"""ABCDEFGHIJKLMNPQRSTUVWXYZ123456789123456789AEIOU""" PASSLEN = 6 RNG = random.Random(time.time()) def generate_password(): """This function creates a pseudo random number generator object, seeded with the cryptographic hash of the passString. The contents of the character set is then shuffled and a selection of passLength words is made from this list. This selection is returned as the generated password.""" l = list(ALPHABET) # make this mutable so that we can shuffle the characters RNG.shuffle(l) # shuffle the character set # pick up only a subset from the available characters: return "".join(RNG.sample(l, PASSLEN)) def mail_password(u, context=None, reset=False): "Send password by email" if not u["email"]: return u[ "url" ] = ( scu.ScoURL() ) # TODO set auth page URL ? (shared by all departments) ../auth/login txt = ( """ Bonjour %(prenom)s %(nom)s, """ % u ) if reset: txt += ( """ votre mot de passe ScoDoc a été ré-initialisé. Le nouveau mot de passe est: %(passwd)s Votre nom d'utilisateur est %(user_name)s Vous devrez changer ce mot de passe lors de votre première connexion sur %(url)s """ % u ) else: txt += ( """ vous avez été déclaré comme utilisateur du logiciel de gestion de scolarité ScoDoc. Votre nom d'utilisateur est %(user_name)s Votre mot de passe est: %(passwd)s Le logiciel est accessible sur: %(url)s Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil). """ % u ) txt += ( """ ScoDoc est un logiciel libre développé à l'Université Paris 13 par Emmanuel Viennet. Pour plus d'informations sur ce logiciel, voir %s """ % scu.SCO_WEBSITE ) msg = MIMEMultipart() if reset: msg["Subject"] = Header("Mot de passe ScoDoc", scu.SCO_ENCODING) else: msg["Subject"] = Header("Votre accès ScoDoc", scu.SCO_ENCODING) msg["From"] = sco_preferences.get_preference("email_from_addr") msg["To"] = u["email"] msg.epilogue = "" txt = MIMEText(txt, "plain", scu.SCO_ENCODING) msg.attach(txt) # sco_emails.sendEmail(msg) # TODO ScoDoc9 pending function