ScoDoc/app/scodoc/sco_import_users.py

345 lines
12 KiB
Python

# -*- 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
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
from app import email
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) <br/> (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:
subject = "Mot de passe ScoDoc"
else:
subject = "Votre accès ScoDoc"
sender = sco_preferences.get_preference("email_from_addr")
email.send_email(subject, sender, [u["email"]], txt)