From 96788d588e5d6658444cab1f52033b7d3d251564 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 22 Aug 2021 13:24:36 +0200
Subject: [PATCH] gestion responsables de semestres
---
app/scodoc/sco_excel.py | 26 ++++--
app/scodoc/sco_import_users.py | 146 ++++++++++++++++++++++++---------
app/scodoc/sco_users.py | 13 ++-
app/views/users.py | 91 ++++++++++++++++++--
4 files changed, 224 insertions(+), 52 deletions(-)
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index d04a1fb61..9da67ec01 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -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="@")
diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py
index 5280559ee..655a16c2b 100644
--- a/app/scodoc/sco_import_users.py
+++ b/app/scodoc/sco_import_users.py
@@ -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,9 +137,17 @@ def import_users(U, auth_dept="", context=None):
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))
+
created = [] # liste de uid créés
+ msg_list = []
+ line = 1 # satr from excel line #2
+ ok = True
try:
- for u in U:
+ for u in users:
+ line = line + 1
ok, msg = sco_users.check_modif_user(
0,
user_name=u["user_name"],
@@ -124,27 +157,60 @@ def import_users(U, auth_dept="", context=None):
roles=u["roles"],
)
if not ok:
- raise ScoValueError(
- "données invalides pour %s: %s" % (u["user_name"], msg)
- )
+ 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()
- # 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
+ # 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 +239,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 +300,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
diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py
index 0068d14ab..65bdfe238 100644
--- a/app/scodoc/sco_users.py
+++ b/app/scodoc/sco_users.py
@@ -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(
- ' Importer des utilisateurs
'.format(
- url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
+ if current_user.is_administrator():
+ H.append(
+ ' Importer des utilisateurs'.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:
diff --git a/app/views/users.py b/app/views/users.py
index 7238da64b..983ab5a4c 100644
--- a/app/views/users.py
+++ b/app/views/users.py
@@ -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,
+ """Téléchargement d'une nouvelle liste d'utilisateurs
+ A utiliser pour importer de nouveaux utilisateurs (enseignants ou secrétaires)
+
+
+ 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.
+
+ """,
+ ]
+ help = """
+ Lors de la creation des utilisateurs, les opérations suivantes sont effectuées:
+
+
+ - vérification des données;
+ - génération d'un mot de passe alétoire pour chaque utilisateur;
+ - création de chaque utilisateur;
+ - envoi à chaque utilisateur de son mot de passe initial par mail.
+
"""
+ H.append(
+ """-
+ Obtenir la feuille excel à remplir
- """
+ )
+ 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] + "
" + 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("")
+ for d in diag:
+ H.append("- %s
" % d)
+ H.append("
")
+ if ok:
+ dest = url_for("users.index_html", scodoc_dept=g.scodoc_dept)
+ H.append("Ok, Import terminé !
")
+ H.append('Continuer
' % dest)
+ else:
+ dest = url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
+ H.append("Erreur, importation annulée !
")
+ H.append('Continuer
' % dest)
+ return "\n".join(H) + html_sco_header.sco_footer()
+ return "\n".join(H) + help + F
@bp.route("/user_info_page")