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:
commit
21c4fb6451
@ -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="@")
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
' <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(
|
||||
' <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
|
||||
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
|
||||
)
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
" Pour importer des utilisateurs en masse (via xlsx file) contactez votre administrateur scodoc."
|
||||
)
|
||||
)
|
||||
if all_depts:
|
||||
checked = "checked"
|
||||
else:
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user