339 lines
11 KiB
Python
339 lines
11 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2023 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
|
|
import time
|
|
|
|
from flask import url_for
|
|
from flask_login import current_user
|
|
|
|
from app import db
|
|
from app import email
|
|
from app.auth.models import User, UserRole
|
|
import app.scodoc.sco_utils as scu
|
|
from app import log
|
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
|
from app.scodoc import sco_excel
|
|
from app.scodoc import sco_users
|
|
|
|
|
|
TITLES = (
|
|
"user_name",
|
|
"nom",
|
|
"prenom",
|
|
"email",
|
|
"roles",
|
|
"dept",
|
|
"cas_id",
|
|
"cas_allow_login",
|
|
"cas_allow_scodoc_login",
|
|
)
|
|
COMMENTS = (
|
|
"""user_name:
|
|
L'identifiant (login).
|
|
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 rôle est fait de 2 composantes séparées par _:
|
|
1. Le rôle (Ens, Secr ou Admin)
|
|
2. Le département (en majuscule)
|
|
Exemple: "Ens_RT,Admin_INFO".
|
|
""",
|
|
"""dept:
|
|
Le département d'appartenance de l'utilisateur. Laisser vide si l'utilisateur
|
|
intervient dans plusieurs départements.
|
|
""",
|
|
"""cas_id:
|
|
identifiant de l'utilisateur sur CAS (optionnel).
|
|
""",
|
|
"""cas_allow_login:
|
|
autorise la connexion via CAS (optionnel, faux par défaut)
|
|
""",
|
|
"""cas_allow_scodoc_login
|
|
autorise connexion via ScoDoc même si CAS obligatoire (optionnel, faux par défaut)
|
|
""",
|
|
)
|
|
|
|
|
|
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 read_users_excel_file(datafile, titles=TITLES) -> list[dict]:
|
|
"""Lit le fichier excel et renvoie liste de dict
|
|
titles : titres (ids) des colonnes attendues
|
|
"""
|
|
# 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:
|
|
raise ScoValueError("Le fichier xlsx attendu semble vide !")
|
|
# 1- --- check title line
|
|
xls_titles = [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 xls_titles:
|
|
if tit not in cols:
|
|
unknown.append(tit)
|
|
else:
|
|
del cols[tit]
|
|
if cols or unknown:
|
|
raise ScoValueError(
|
|
f"""colonnes incorrectes (on attend {len(titles)}, et non {len(xls_titles)})
|
|
<br>
|
|
(colonnes manquantes: {list(cols.keys())}, colonnes invalides: {unknown})
|
|
"""
|
|
)
|
|
# ok, same titles... : build the list of dictionaries
|
|
users = []
|
|
for line in data[1:]:
|
|
d = {}
|
|
for i, field in enumerate(xls_titles):
|
|
d[field] = line[i]
|
|
users.append(d)
|
|
return users
|
|
|
|
|
|
def import_excel_file(datafile, force="") -> tuple[bool, list[str], int]:
|
|
"""
|
|
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 department (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
|
|
|
|
:param datafile: the stream to be imported
|
|
:return: same as import users
|
|
"""
|
|
if not current_user.is_administrator():
|
|
raise AccessDenied(f"invalid user ({current_user}) must be SuperAdmin")
|
|
|
|
log("sco_import_users.import_excel_file by {current_user}")
|
|
|
|
users = read_users_excel_file(datafile)
|
|
|
|
return import_users(users=users, force=force)
|
|
|
|
|
|
def import_users(users, force="") -> tuple[bool, list[str], int]:
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
created = {} # uid créés
|
|
if len(users) == 0:
|
|
import_ok = False
|
|
msg_list = ["Feuille vide ou illisible"]
|
|
else:
|
|
msg_list = []
|
|
line = 1 # start from excel line #2
|
|
import_ok = True
|
|
|
|
def append_msg(msg):
|
|
msg_list.append(f"Ligne {line} : {msg}")
|
|
|
|
try:
|
|
for u in users:
|
|
line = line + 1
|
|
user_ok, msg = sco_users.check_modif_user(
|
|
0,
|
|
enforce_optionals=not force,
|
|
user_name=u["user_name"],
|
|
nom=u["nom"],
|
|
prenom=u["prenom"],
|
|
email=u["email"],
|
|
roles=[r for r in u["roles"].split(",") if r],
|
|
dept=u["dept"],
|
|
cas_id=u["cas_id"],
|
|
)
|
|
if not user_ok:
|
|
append_msg(f"""identifiant '{u["user_name"]}' {msg}""")
|
|
|
|
u["passwd"] = generate_password()
|
|
#
|
|
# check identifiant
|
|
if u["user_name"] in created.keys():
|
|
user_ok = False
|
|
append_msg(
|
|
f"""l'utilisateur '{u["user_name"]}' a déjà été décrit ligne {
|
|
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 = []
|
|
for role in u["roles"].split(","):
|
|
try:
|
|
role = role.strip()
|
|
if role:
|
|
_, _ = UserRole.role_dept_from_string(role)
|
|
roles_list.append(role)
|
|
except ScoValueError as value_error:
|
|
user_ok = False
|
|
append_msg(f"role {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(f"import_users: exception: abort create {str(created.keys())}")
|
|
raise ScoValueError(msg) from value_error
|
|
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 = 8
|
|
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(user: dict, reset=False) -> None:
|
|
"Send password by email"
|
|
if not user["email"]:
|
|
return
|
|
|
|
user["url"] = url_for("scodoc.index", _external=True)
|
|
txt = (
|
|
"""
|
|
Bonjour %(prenom)s %(nom)s,
|
|
|
|
"""
|
|
% user
|
|
)
|
|
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
|
|
"""
|
|
% user
|
|
)
|
|
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).
|
|
"""
|
|
% user
|
|
)
|
|
|
|
txt += (
|
|
"""
|
|
_______
|
|
ScoDoc est un logiciel libre développé par Emmanuel Viennet et l'association ScoDoc.
|
|
Pour plus d'informations sur ce logiciel, voir %s
|
|
|
|
"""
|
|
% scu.SCO_WEBSITE
|
|
)
|
|
|
|
if reset:
|
|
subject = "Mot de passe ScoDoc"
|
|
else:
|
|
subject = "Votre accès ScoDoc"
|
|
email.send_email(subject, email.get_from_addr(), [user["email"]], txt)
|