# -*- 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): "Create users from Excel file" # 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) 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... U = [] for line in data[1:]: d = {} for i in range(len(fs)): d[fs[i]] = line[i] U.append(d) return import_users(U, auth_dept=auth_dept) def import_users(users, auth_dept=""): """Import des utilisateurs: Pour chaque utilisateur à créer: - vérifier données - générer mot de passe aléatoire - créer utilisateur et mettre le mot de passe - envoyer mot de passe par mail En cas d'erreur: supprimer tous les utilisateurs que l'on vient de créer. """ def append_msg(msg): msg_list.append("Ligne %s : %s" % (line, msg)) if len(users) == 0: ok = False msg_list = ["Feuilles vide ou illisible"] else: created = [] # liste de uid créés msg_list = [] line = 1 # satr from excel line #2 ok = True try: for u in users: line = line + 1 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"], ) if not 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"]): ok = False append_msg( "identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)" % u["user_name"] ) elif len(u["user_name"]) > 64: ok = False append_msg( "identifiant '%s' trop long (64 caractères)" % u["user_name"] ) if len(u["nom"]) > 64: ok = False append_msg("nom '%s' trop long (64 caractères)" % u["nom"]) if len(u["prenom"]) > 64: ok = False append_msg("prenom '%s' trop long (64 caractères)" % u["prenom"]) if len(u["email"]) > 120: ok = False append_msg("email '%s' trop long (120 caractères)" % u["email"]) # check département if u["dept"] != "": dept = Departement.query.filter_by(acronym=u["dept"]).first() if dept is None: ok = False append_msg("département '%s' inexistant" % u["dept"]) for role in u["roles"].split(","): try: _, _ = UserRole.role_dept_from_string(role) except ScoValueError as value_error: ok = False append_msg("role : %s " % role) # Création de l'utilisateur (via SQLAlchemy) if ok: user = User() user.from_dict(u, new_user=True) db.session.add(user) created.append(u["user_name"]) db.session.commit() except ScoValueError as value_error: log("import_users: exception: abort create %s" % str(created)) db.session.rollback() raise ScoValueError(msg) # re-raise exception for user in users: mail_password(user) return ok, msg_list # --------- 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