1
0
forked from ScoDoc/ScoDoc

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 import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.comments import Comment
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import notesdb from app.scodoc import notesdb
@ -257,7 +258,7 @@ class ScoExcelSheet:
""" """
self.ws.column_dimensions[cle].hidden = value 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. """Construit une cellule.
value -- contenu de la cellule (texte ou numérique) 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é 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"] cell.fill = style["fill"]
if "alignment" in style: if "alignment" in style:
cell.alignment = style["alignment"] cell.alignment = style["alignment"]
if not comment is None:
cell.comment = Comment(comment, "scodoc")
cell.comment.width = 400
cell.comment.height = 150
return cell return cell
def make_row(self, values: list, style=None): def make_row(self, values: list, style=None, comments=None):
return [self.make_cell(value, style) for value in values] # 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): def append_single_cell_row(self, value: any, style=None):
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille. """construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
@ -367,7 +378,7 @@ class ScoExcelSheet:
def excel_simple_table( 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""" """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel"""
ws = ScoExcelSheet(sheet_name) ws = ScoExcelSheet(sheet_name)
@ -378,9 +389,14 @@ def excel_simple_table(
if titles_styles is None: if titles_styles is None:
style = excel_make_style(bold=True) style = excel_make_style(bold=True)
titles_styles = [style] * len(titles) titles_styles = [style] * len(titles)
if comments is None:
comments = [None] * len(titles)
# ligne de titres # ligne de titres
ws.append_row( 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() default_style = excel_make_style()
text_style = excel_make_style(format_number="@") text_style = excel_make_style(format_number="@")

View File

@ -28,11 +28,13 @@
"""Import d'utilisateurs via fichier Excel """Import d'utilisateurs via fichier Excel
""" """
import random, time import random, time
import re
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.header import Header from email.header import Header
from app import db, Departement
from app.scodoc import sco_emails from app.scodoc import sco_emails
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.notes_log import log 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_preferences
from app.scodoc import sco_users 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") 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(): def generate_excel_sample():
"""generates an excel document suitable to import users""" """generates an excel document suitable to import users"""
style = sco_excel.excel_make_style(bold=True) style = sco_excel.excel_make_style(bold=True)
titles = TITLES titles = TITLES
titlesStyles = [style] * len(titles) titles_styles = [style] * len(titles)
return sco_excel.excel_simple_table( 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" "Create users from Excel file"
authuser = REQUEST.AUTHENTICATED_USER # Check current user privilege
auth_name = str(authuser) auth_dept = current_user.dept
authuser_info = context._user_list(args={"user_name": auth_name}) auth_name = str(current_user)
zope_roles = authuser.getRolesInContext(context) if not current_user.is_administrator():
if not authuser_info and not ("Manager" in zope_roles): raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
# not admin, and not in database # Récupération des informations sur l'utilisateur courant
raise AccessDenied("invalid user (%s)" % auth_name)
if authuser_info:
auth_dept = authuser_info[0]["dept"]
else:
auth_dept = ""
log("sco_import_users.import_excel_file by %s" % auth_name) log("sco_import_users.import_excel_file by %s" % auth_name)
exceldata = datafile.read() exceldata = datafile.read()
if not exceldata: if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide") 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 if not data: # probably a bug
raise ScoException("import_excel_file: empty file !") raise ScoException("import_excel_file: empty file !")
# 1- --- check title line # 1- --- check title line
@ -99,10 +124,10 @@ def import_excel_file(datafile, REQUEST=None, context=None):
d[fs[i]] = line[i] d[fs[i]] = line[i]
U.append(d) 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: """Import des utilisateurs:
Pour chaque utilisateur à créer: Pour chaque utilisateur à créer:
- vérifier données - vérifier données
@ -112,9 +137,21 @@ def import_users(U, auth_dept="", context=None):
En cas d'erreur: supprimer tous les utilisateurs que l'on vient de créer. 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))
if len(users) == 0:
ok = False
msg_list = ["Feuilles vide ou illisible"]
else:
created = [] # liste de uid créés created = [] # liste de uid créés
msg_list = []
line = 1 # satr from excel line #2
ok = True
try: try:
for u in U: for u in users:
line = line + 1
ok, msg = sco_users.check_modif_user( ok, msg = sco_users.check_modif_user(
0, 0,
user_name=u["user_name"], user_name=u["user_name"],
@ -124,27 +161,60 @@ def import_users(U, auth_dept="", context=None):
roles=u["roles"], roles=u["roles"],
) )
if not ok: if not ok:
raise ScoValueError( append_msg("identifiant '%s' %s" % (u["user_name"], msg))
"données invalides pour %s: %s" % (u["user_name"], msg) # raise ScoValueError(
) # "données invalides pour %s: %s" % (u["user_name"], msg)
# )
u["passwd"] = generate_password() 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()) # 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"]) created.append(u["user_name"])
except: db.session.commit()
log("import_users: exception: deleting %s" % str(created)) except ScoValueError as value_error:
# delete created users log("import_users: exception: abort create %s" % str(created))
for user_name in created: db.session.rollback()
context._user_delete(user_name) raise ScoValueError(msg) # re-raise exception
raise # re-raise exception
for u in U: for user in users:
mail_password(u, context=context) mail_password(user)
return ok, msg_list
return "ok"
# --------- Génération du mot de passe initial ----------- # --------- Génération du mot de passe initial -----------
@ -173,7 +243,11 @@ def mail_password(u, context=None, reset=False):
if not u["email"]: if not u["email"]:
return return
u["url"] = scu.ScoURL() u[
"url"
] = (
scu.ScoURL()
) # TODO set auth page URL ? (shared by all departments) ../auth/login
txt = ( txt = (
""" """
@ -230,4 +304,4 @@ Pour plus d'informations sur ce logiciel, voir %s
msg.epilogue = "" msg.epilogue = ""
txt = MIMEText(txt, "plain", scu.SCO_ENCODING) txt = MIMEText(txt, "plain", scu.SCO_ENCODING)
msg.attach(txt) 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) url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
) )
) )
if current_user.is_administrator():
H.append( H.append(
'&nbsp;&nbsp; <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format( '&nbsp;&nbsp; <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept) 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: if all_depts:
checked = "checked" checked = "checked"
else: else:

View File

@ -38,7 +38,8 @@ import re
from xml.etree import ElementTree from xml.etree import ElementTree
import flask import flask
from flask import g from flask import g, url_for
from flask_login import current_user from flask_login import current_user
from app import db from app import db
@ -54,7 +55,7 @@ from app.decorators import (
permission_required, 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_users
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml 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_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions_check import can_handle_passwd from app.scodoc.sco_permissions_check import can_handle_passwd
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message 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 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") @bp.route("/import_users_generate_excel_sample")
def import_users_form(): @scodoc
raise NotImplementedError() @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") @bp.route("/user_info_page")