2020-09-26 16:19:37 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# Gestion scolarite IUT
|
|
|
|
#
|
2022-01-01 14:49:42 +01:00
|
|
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
"""
|
2021-10-13 10:33:20 +02:00
|
|
|
import random
|
|
|
|
import time
|
2021-07-19 19:53:01 +02:00
|
|
|
|
2021-02-13 23:18:32 +01:00
|
|
|
from email.mime.multipart import MIMEMultipart
|
2021-10-13 10:33:20 +02:00
|
|
|
from flask import g, url_for
|
|
|
|
from flask_login import current_user
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-10-13 10:33:20 +02:00
|
|
|
from app import db
|
|
|
|
from app import email
|
|
|
|
from app.auth.models import User, UserRole
|
2021-06-19 23:21:37 +02:00
|
|
|
import app.scodoc.sco_utils as scu
|
2021-08-29 19:57:32 +02:00
|
|
|
from app import log
|
2021-10-13 10:33:20 +02:00
|
|
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc import sco_excel
|
|
|
|
from app.scodoc import sco_preferences
|
2021-07-03 16:19:42 +02:00
|
|
|
from app.scodoc import sco_users
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-08-26 23:43:54 +02:00
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
|
2021-08-22 13:24:36 +02:00
|
|
|
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
|
|
|
|
""",
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
def generate_excel_sample():
|
2020-12-15 08:48:29 +01:00
|
|
|
"""generates an excel document suitable to import users"""
|
2021-08-12 14:55:25 +02:00
|
|
|
style = sco_excel.excel_make_style(bold=True)
|
2020-09-26 16:19:37 +02:00
|
|
|
titles = TITLES
|
2021-08-22 13:24:36 +02:00
|
|
|
titles_styles = [style] * len(titles)
|
2021-08-12 14:49:53 +02:00
|
|
|
return sco_excel.excel_simple_table(
|
2021-08-22 13:24:36 +02:00
|
|
|
titles=titles,
|
|
|
|
titles_styles=titles_styles,
|
|
|
|
sheet_name="Utilisateurs ScoDoc",
|
|
|
|
comments=COMMENTS,
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-10-13 16:32:43 +02:00
|
|
|
def import_excel_file(datafile, force=""):
|
2021-08-23 07:37:14 +02:00
|
|
|
"""
|
|
|
|
Import scodoc users from Excel file.
|
|
|
|
This method:
|
2021-10-13 10:33:20 +02:00
|
|
|
* 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)
|
2021-08-23 07:37:14 +02:00
|
|
|
* 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
|
|
|
|
"""
|
2021-08-22 13:24:36 +02:00
|
|
|
# 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
|
2020-09-26 16:19:37 +02:00
|
|
|
log("sco_import_users.import_excel_file by %s" % auth_name)
|
2021-08-23 07:37:14 +02:00
|
|
|
# Read the data from the stream
|
2020-09-26 16:19:37 +02:00
|
|
|
exceldata = datafile.read()
|
|
|
|
if not exceldata:
|
|
|
|
raise ScoValueError("Ficher excel vide ou invalide")
|
2021-08-22 13:24:36 +02:00
|
|
|
_, data = sco_excel.excel_bytes_to_list(exceldata)
|
2021-10-10 21:03:18 +02:00
|
|
|
if not data:
|
|
|
|
raise ScoValueError(
|
|
|
|
"""Le fichier xlsx attendu semble vide !
|
|
|
|
"""
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
# 1- --- check title line
|
2021-08-21 00:24:51 +02:00
|
|
|
fs = [scu.stripquotes(s).lower() for s in data[0]]
|
2020-09-26 16:19:37 +02:00
|
|
|
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
|
|
|
|
# check cols
|
|
|
|
cols = {}.fromkeys(TITLES)
|
|
|
|
unknown = []
|
|
|
|
for tit in fs:
|
2021-07-09 17:47:06 +02:00
|
|
|
if tit not in cols:
|
2020-09-26 16:19:37 +02:00
|
|
|
unknown.append(tit)
|
|
|
|
else:
|
|
|
|
del cols[tit]
|
|
|
|
if cols or unknown:
|
|
|
|
raise ScoValueError(
|
2021-10-13 10:33:20 +02:00
|
|
|
"""colonnes incorrectes (on attend %d, et non %d) <br/>
|
|
|
|
(colonnes manquantes: %s, colonnes invalides: %s)"""
|
2021-07-09 17:47:06 +02:00
|
|
|
% (len(TITLES), len(fs), list(cols.keys()), unknown)
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-08-23 07:37:14 +02:00
|
|
|
# ok, same titles... : build the list of dictionaries
|
|
|
|
users = []
|
2020-09-26 16:19:37 +02:00
|
|
|
for line in data[1:]:
|
|
|
|
d = {}
|
|
|
|
for i in range(len(fs)):
|
|
|
|
d[fs[i]] = line[i]
|
2021-08-23 07:37:14 +02:00
|
|
|
users.append(d)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-10-13 16:32:43 +02:00
|
|
|
return import_users(users=users, force=force)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2021-10-13 16:32:43 +02:00
|
|
|
def import_users(users, force=""):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2021-08-23 07:37:14 +02:00
|
|
|
Import users from a list of users_descriptors.
|
2021-08-22 13:24:36 +02:00
|
|
|
|
2021-08-23 07:37:14 +02:00
|
|
|
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
|
2022-07-04 17:23:51 +02:00
|
|
|
|
|
|
|
Implémentation:
|
2021-08-23 07:37:14 +02:00
|
|
|
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
|
2022-07-04 17:23:51 +02:00
|
|
|
Les utilisateurs à créer sont stockés dans un dictionnaire.
|
2021-08-23 07:37:14 +02:00
|
|
|
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
|
|
|
|
"""
|
2021-08-22 13:24:36 +02:00
|
|
|
|
2022-07-04 17:23:51 +02:00
|
|
|
created = {} # uid créés
|
2021-08-22 15:25:25 +02:00
|
|
|
if len(users) == 0:
|
2021-08-22 19:17:43 +02:00
|
|
|
import_ok = False
|
2021-08-23 07:37:14 +02:00
|
|
|
msg_list = ["Feuille vide ou illisible"]
|
2021-08-22 15:25:25 +02:00
|
|
|
else:
|
|
|
|
msg_list = []
|
2021-08-23 07:37:14 +02:00
|
|
|
line = 1 # start from excel line #2
|
2021-08-22 19:17:43 +02:00
|
|
|
import_ok = True
|
2021-08-23 07:37:14 +02:00
|
|
|
|
|
|
|
def append_msg(msg):
|
|
|
|
msg_list.append("Ligne %s : %s" % (line, msg))
|
|
|
|
|
2021-08-22 15:25:25 +02:00
|
|
|
try:
|
|
|
|
for u in users:
|
|
|
|
line = line + 1
|
2021-08-22 19:17:43 +02:00
|
|
|
user_ok, msg = sco_users.check_modif_user(
|
2021-08-22 15:25:25 +02:00
|
|
|
0,
|
2021-10-13 16:32:43 +02:00
|
|
|
enforce_optionals=not force,
|
2021-08-22 15:25:25 +02:00
|
|
|
user_name=u["user_name"],
|
|
|
|
nom=u["nom"],
|
|
|
|
prenom=u["prenom"],
|
|
|
|
email=u["email"],
|
2021-10-13 15:03:41 +02:00
|
|
|
roles=[r for r in u["roles"].split(",") if r],
|
2021-10-10 10:52:06 +02:00
|
|
|
dept=u["dept"],
|
2021-08-22 13:24:36 +02:00
|
|
|
)
|
2021-08-22 19:17:43 +02:00
|
|
|
if not user_ok:
|
2021-08-22 15:25:25 +02:00
|
|
|
append_msg("identifiant '%s' %s" % (u["user_name"], msg))
|
2021-10-13 10:33:20 +02:00
|
|
|
|
2021-08-22 15:25:25 +02:00
|
|
|
u["passwd"] = generate_password()
|
|
|
|
#
|
|
|
|
# check identifiant
|
2021-08-23 07:37:14 +02:00
|
|
|
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 roles / ignore whitespaces around roles / build roles_string
|
|
|
|
# roles_string (expected by User) appears as column 'roles' in excel file
|
|
|
|
roles_list = []
|
2021-08-22 15:25:25 +02:00
|
|
|
for role in u["roles"].split(","):
|
|
|
|
try:
|
2021-09-29 10:27:49 +02:00
|
|
|
role = role.strip()
|
|
|
|
if role:
|
|
|
|
_, _ = UserRole.role_dept_from_string(role)
|
|
|
|
roles_list.append(role)
|
2021-08-22 15:25:25 +02:00
|
|
|
except ScoValueError as value_error:
|
2021-08-22 19:17:43 +02:00
|
|
|
user_ok = False
|
2021-08-23 07:37:14 +02:00
|
|
|
append_msg("role %s : %s" % (role, value_error))
|
|
|
|
u["roles_string"] = ",".join(roles_list)
|
2021-08-22 19:17:43 +02:00
|
|
|
if user_ok:
|
2021-08-23 07:37:14 +02:00
|
|
|
u["line"] = line
|
|
|
|
created[u["user_name"]] = u
|
2021-08-22 19:17:43 +02:00
|
|
|
else:
|
|
|
|
import_ok = False
|
2021-08-22 15:25:25 +02:00
|
|
|
except ScoValueError as value_error:
|
2022-07-04 17:23:51 +02:00
|
|
|
log(f"import_users: exception: abort create {str(created.keys())}")
|
2021-10-13 10:33:20 +02:00
|
|
|
raise ScoValueError(msg) from value_error
|
2021-08-22 19:17:43 +02:00
|
|
|
if import_ok:
|
2021-08-23 07:37:14 +02:00
|
|
|
for u in created.values():
|
|
|
|
# Création de l'utilisateur (via SQLAlchemy)
|
2021-08-22 19:17:43 +02:00
|
|
|
user = User()
|
|
|
|
user.from_dict(u, new_user=True)
|
|
|
|
db.session.add(user)
|
|
|
|
db.session.commit()
|
|
|
|
mail_password(u)
|
|
|
|
else:
|
2022-07-04 17:23:51 +02:00
|
|
|
created = {} # reset # of created users to 0
|
2021-08-22 19:17:43 +02:00
|
|
|
return import_ok, msg_list, len(created)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
# --------- 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"""
|
2021-10-13 10:33:20 +02:00
|
|
|
PASSLEN = 8
|
2020-09-26 16:19:37 +02:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
2021-10-13 10:33:20 +02:00
|
|
|
def mail_password(user: dict, reset=False) -> None:
|
2020-09-26 16:19:37 +02:00
|
|
|
"Send password by email"
|
2021-10-13 10:33:20 +02:00
|
|
|
if not user["email"]:
|
2020-09-26 16:19:37 +02:00
|
|
|
return
|
|
|
|
|
2021-10-13 21:49:55 +02:00
|
|
|
user["url"] = url_for("scodoc.index", _external=True)
|
2020-09-26 16:19:37 +02:00
|
|
|
txt = (
|
|
|
|
"""
|
|
|
|
Bonjour %(prenom)s %(nom)s,
|
|
|
|
|
|
|
|
"""
|
2021-10-13 10:33:20 +02:00
|
|
|
% user
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
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
|
|
|
|
|
2021-10-13 10:33:20 +02:00
|
|
|
Vous devrez changer ce mot de passe lors de votre première connexion
|
2020-09-26 16:19:37 +02:00
|
|
|
sur %(url)s
|
|
|
|
"""
|
2021-10-13 10:33:20 +02:00
|
|
|
% user
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
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
|
|
|
|
|
2021-10-13 21:49:55 +02:00
|
|
|
Vous êtes invité à changer ce mot de passe au plus vite (cliquez sur votre nom en haut à gauche de la page d'accueil).
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2021-10-13 10:33:20 +02:00
|
|
|
% user
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
txt += (
|
|
|
|
"""
|
2021-10-13 21:49:55 +02:00
|
|
|
_______
|
2021-10-13 10:33:20 +02:00
|
|
|
ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
|
2020-09-26 16:19:37 +02:00
|
|
|
Pour plus d'informations sur ce logiciel, voir %s
|
|
|
|
|
|
|
|
"""
|
2021-02-04 20:02:44 +01:00
|
|
|
% scu.SCO_WEBSITE
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
|
|
|
msg = MIMEMultipart()
|
|
|
|
if reset:
|
2021-08-26 23:43:54 +02:00
|
|
|
subject = "Mot de passe ScoDoc"
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2021-08-26 23:43:54 +02:00
|
|
|
subject = "Votre accès ScoDoc"
|
|
|
|
sender = sco_preferences.get_preference("email_from_addr")
|
2021-10-13 10:33:20 +02:00
|
|
|
email.send_email(subject, sender, [user["email"]], txt)
|