ScoDoc/app/scodoc/sco_import_users.py

304 lines
9.8 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
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) <br/> (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))
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