Merge pull request 'scodoc9_import_utilisateurs' (#109) from jmplace/ScoDoc-Lille:scodoc9_import_utilisateurs into master

Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/109
This commit is contained in:
Emmanuel Viennet 2021-08-22 17:20:02 +02:00
commit 21c4fb6451
4 changed files with 240 additions and 64 deletions

View File

@ -38,6 +38,7 @@ import openpyxl.utils.datetime
from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.comments import Comment
import app.scodoc.sco_utils as scu
from app.scodoc import notesdb
@ -257,7 +258,7 @@ class ScoExcelSheet:
"""
self.ws.column_dimensions[cle].hidden = value
def make_cell(self, value: any = None, style=None):
def make_cell(self, value: any = None, style=None, comment=None):
"""Construit une cellule.
value -- contenu de la cellule (texte ou numérique)
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
@ -277,10 +278,20 @@ class ScoExcelSheet:
cell.fill = style["fill"]
if "alignment" in style:
cell.alignment = style["alignment"]
if not comment is None:
cell.comment = Comment(comment, "scodoc")
cell.comment.width = 400
cell.comment.height = 150
return cell
def make_row(self, values: list, style=None):
return [self.make_cell(value, style) for value in values]
def make_row(self, values: list, style=None, comments=None):
# TODO make possible differents styles in a row
if comments is None:
comments = [None] * len(values)
return [
self.make_cell(value, style, comment)
for value, comment in zip(values, comments)
]
def append_single_cell_row(self, value: any, style=None):
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
@ -367,7 +378,7 @@ class ScoExcelSheet:
def excel_simple_table(
titles=None, lines=None, sheet_name=b"feuille", titles_styles=None
titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None
):
"""Export simple type 'CSV': 1ere ligne en gras, le reste tel quel"""
ws = ScoExcelSheet(sheet_name)
@ -378,9 +389,14 @@ def excel_simple_table(
if titles_styles is None:
style = excel_make_style(bold=True)
titles_styles = [style] * len(titles)
if comments is None:
comments = [None] * len(titles)
# ligne de titres
ws.append_row(
[ws.make_cell(it, style) for (it, style) in zip(titles, titles_styles)]
[
ws.make_cell(it, style, comment)
for (it, style, comment) in zip(titles, titles_styles, comments)
]
)
default_style = excel_make_style()
text_style = excel_make_style(format_number="@")

View File

@ -28,11 +28,13 @@
"""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
@ -41,38 +43,61 @@ 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
titlesStyles = [style] * len(titles)
titles_styles = [style] * len(titles)
return sco_excel.excel_simple_table(
titles=titles, titlesStyles=titlesStyles, sheet_name="Utilisateurs ScoDoc"
titles=titles,
titles_styles=titles_styles,
sheet_name="Utilisateurs ScoDoc",
comments=COMMENTS,
)
def import_excel_file(datafile, REQUEST=None, context=None):
def import_excel_file(datafile):
"Create users from Excel file"
authuser = REQUEST.AUTHENTICATED_USER
auth_name = str(authuser)
authuser_info = context._user_list(args={"user_name": auth_name})
zope_roles = authuser.getRolesInContext(context)
if not authuser_info and not ("Manager" in zope_roles):
# not admin, and not in database
raise AccessDenied("invalid user (%s)" % auth_name)
if authuser_info:
auth_dept = authuser_info[0]["dept"]
else:
auth_dept = ""
# 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_to_list(exceldata)
_, 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
@ -99,10 +124,10 @@ def import_excel_file(datafile, REQUEST=None, context=None):
d[fs[i]] = line[i]
U.append(d)
return import_users(U, auth_dept=auth_dept, context=context)
return import_users(U, auth_dept=auth_dept)
def import_users(U, auth_dept="", context=None):
def import_users(users, auth_dept=""):
"""Import des utilisateurs:
Pour chaque utilisateur à créer:
- vérifier données
@ -112,39 +137,84 @@ def import_users(U, auth_dept="", context=None):
En cas d'erreur: supprimer tous les utilisateurs que l'on vient de créer.
"""
created = [] # liste de uid créés
try:
for u in U:
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:
raise ScoValueError(
"données invalides pour %s: %s" % (u["user_name"], msg)
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"],
)
u["passwd"] = generate_password()
# si auth_dept, crée tous les utilisateurs dans ce departement
if auth_dept:
u["dept"] = auth_dept
#
context.create_user(u.copy())
created.append(u["user_name"])
except:
log("import_users: exception: deleting %s" % str(created))
# delete created users
for user_name in created:
context._user_delete(user_name)
raise # re-raise exception
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 u in U:
mail_password(u, context=context)
return "ok"
for user in users:
mail_password(user)
return ok, msg_list
# --------- Génération du mot de passe initial -----------
@ -173,7 +243,11 @@ def mail_password(u, context=None, reset=False):
if not u["email"]:
return
u["url"] = scu.ScoURL()
u[
"url"
] = (
scu.ScoURL()
) # TODO set auth page URL ? (shared by all departments) ../auth/login
txt = (
"""
@ -230,4 +304,4 @@ Pour plus d'informations sur ce logiciel, voir %s
msg.epilogue = ""
txt = MIMEText(txt, "plain", scu.SCO_ENCODING)
msg.attach(txt)
sco_emails.sendEmail(msg)
# sco_emails.sendEmail(msg) # TODO ScoDoc9 pending function

View File

@ -94,11 +94,16 @@ def index_html(REQUEST, all_depts=False, with_inactives=False, format="html"):
url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
)
)
H.append(
'&nbsp;&nbsp; <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
if current_user.is_administrator():
H.append(
'&nbsp;&nbsp; <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
)
)
else:
H.append(
"&nbsp;&nbsp; Pour importer des utilisateurs en masse (via xlsx file) contactez votre administrateur scodoc."
)
)
if all_depts:
checked = "checked"
else:

View File

@ -38,7 +38,8 @@ import re
from xml.etree import ElementTree
import flask
from flask import g
from flask import g, url_for
from flask_login import current_user
from app import db
@ -54,7 +55,7 @@ from app.decorators import (
permission_required,
)
from app.scodoc import html_sco_header
from app.scodoc import html_sco_header, sco_import_users, sco_excel
from app.scodoc import sco_users
from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml
@ -62,6 +63,8 @@ from app.scodoc.notes_log import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions_check import can_handle_passwd
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_excel import send_excel_file
from app.scodoc.sco_import_users import generate_excel_sample
from app.views import users_bp as bp
@ -459,9 +462,87 @@ def create_user_form(REQUEST, user_name=None, edit=0):
)
@bp.route("/import_users_form")
def import_users_form():
raise NotImplementedError()
@bp.route("/import_users_generate_excel_sample")
@scodoc
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func
def import_users_generate_excel_sample(REQUEST):
"une feuille excel pour importation utilisateurs"
data = sco_import_users.generate_excel_sample()
return sco_excel.send_excel_file(REQUEST, data, "ImportUtilisateurs")
@bp.route("/import_users_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func
def import_users_form(REQUEST=None):
"""Import utilisateurs depuis feuille Excel"""
head = html_sco_header.sco_header(page_title="Import utilisateurs")
H = [
head,
"""<h2>Téléchargement d'une nouvelle liste d'utilisateurs</h2>
<p style="color: red">A utiliser pour importer de <b>nouveaux</b> utilisateurs (enseignants ou secrétaires)
</p>
<p>
L'opération se déroule en deux étapes. Dans un premier temps,
vous téléchargez une feuille Excel type. Vous devez remplir
cette feuille, une ligne décrivant chaque utilisateur. Ensuite,
vous indiquez le nom de votre fichier dans la case "Fichier Excel"
ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
votre liste.
</p>
""",
]
help = """<p class="help">
Lors de la creation des utilisateurs, les opérations suivantes sont effectuées:
</p>
<ol class="help">
<li>vérification des données;</li>
<li>génération d'un mot de passe alétoire pour chaque utilisateur;</li>
<li>création de chaque utilisateur;</li>
<li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li>
</ol>"""
H.append(
"""<ol><li><a class="stdlink" href="import_users_generate_excel_sample">
Obtenir la feuille excel à remplir</a></li><li>"""
)
F = html_sco_header.sco_footer()
tf = TrivialFormulator(
REQUEST.URL0,
REQUEST.form,
(
(
"xlsfile",
{"title": "Fichier Excel:", "input_type": "file", "size": 40},
),
("formsemestre_id", {"input_type": "hidden"}),
),
submitlabel="Télécharger",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + "</li></ol>" + help + F
elif tf[0] == -1:
return flask.redirect(back_url)
else:
# IMPORT
ok, diag = sco_import_users.import_excel_file(tf[2]["xlsfile"])
# TODO Afficher la liste des messages
H = [html_sco_header.sco_header(page_title="Import utilisateurs")]
H.append("<ul>")
for d in diag:
H.append("<li>%s</li>" % d)
H.append("</ul>")
if ok:
dest = url_for("users.index_html", scodoc_dept=g.scodoc_dept)
H.append("<p>Ok, Import terminé !</p>")
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
else:
dest = url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
H.append("<p>Erreur, importation annulée !</p>")
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
return "\n".join(H) + html_sco_header.sco_footer()
return "\n".join(H) + help + F
@bp.route("/user_info_page")