diff --git a/app/auth/models.py b/app/auth/models.py index e4ae2fc174..395f80be39 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -20,6 +20,8 @@ from app import db, login from app.scodoc.sco_permissions import Permission from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS +import app.scodoc.sco_utils as scu +from app.scodoc import sco_etud # a deplacer dans scu class User(UserMixin, db.Model): @@ -207,6 +209,21 @@ class User(UserMixin, db.Model): def is_administrator(self): return self.active and self.has_permission(Permission.ScoSuperAdmin, None) + # Some useful strings: + def get_nomplogin(self): + """nomplogin est le nom en majuscules suivi du prénom et du login + e.g. Dupont Pierre (dupont) + """ + if self.nom: + n = sco_etud.format_nom(self.nom) + else: + n = scu.strupper(self.user_name) + return "%s %s (%s)" % ( + n, + sco_etud.format_prenom(self.prenom), + self.user_name, + ) + class AnonymousUser(AnonymousUserMixin): def has_permission(self, perm, dept=None): diff --git a/app/decorators.py b/app/decorators.py index 0fe4f46c3e..c522768201 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -18,7 +18,6 @@ from flask_login import login_required from flask import current_app import app -from app.auth.models import Permission class ZUser(object): @@ -111,6 +110,8 @@ def permission_required(permission): def admin_required(f): + from app.auth.models import Permission + return permission_required(Permission.ScoSuperAdmin)(f) diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index cc48c167a5..7a426a12a4 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -46,7 +46,7 @@ def sidebar_common(context, REQUEST=None): "authuser": str(authuser), } H = [ - 'ScoDoc', + 'ScoDoc 8', '
%(authuser)s
déconnexion
' % params, sidebar_dept(context, REQUEST), diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 1a95d89776..e31be3afe5 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -140,10 +140,10 @@ def do_formsemestre_createwithmodules(context, REQUEST=None, edit=False): ) # Liste des enseignants avec forme pour affichage / saisie avec suggestion - userlist = [sco_users.user_info(u) for u in sco_users.get_user_list()] + userlist = sco_users.get_user_list() login2display = {} # user_name : forme pour affichage = "NOM Prenom (login)" for u in userlist: - login2display[u["user_name"]] = u["nomplogin"] + login2display[u.user_name] = u.get_nomplogin() allowed_user_names = login2display.values() + [""] # formation_id = REQUEST.form["formation_id"] diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index 5a8235a3bf..d43f229e79 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -261,7 +261,7 @@ def _user_list(user_name): def user_info(user_name=None, user=None): - """Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base). + """Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base). Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict. """ if user_name: @@ -294,10 +294,7 @@ def user_info(user_name=None, user=None): if "password_hash" in info: del info["password_hash"] # - if info["prenom"]: - p = format_prenom(info["prenom"]) - else: - p = "" + p = format_prenom(info["prenom"]) if info["nom"]: n = format_nom( info["nom"], uppercase=False diff --git a/app/views/notes.py b/app/views/notes.py index a02976face..67a7827798 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -663,6 +663,7 @@ sco_publish( "/formsemestre_edit_options", sco_formsemestre_edit.formsemestre_edit_options, Permission.ScoView, + methods=["GET", "POST"], ) sco_publish( "/formsemestre_change_lock", diff --git a/app/views/users.py b/app/views/users.py index 24a2899b2e..cbdaddeab6 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -34,8 +34,11 @@ Vues s'appuyant sur auth et sco_users Emmanuel Viennet, 2021 """ +import jaxml + from flask import g from flask import current_app, request +from flask_login import current_user from app.auth.models import Permission from app.auth.models import User @@ -48,8 +51,10 @@ from app.decorators import ( ZRequest, ) +from app.scodoc import html_sco_header from app.scodoc import sco_users from app.scodoc import sco_utils as scu +from app.scodoc.notes_log import log from app.views import users_bp as bp @@ -79,9 +84,320 @@ def user_info(user_name, format="json", REQUEST=None): return scu.sendResult(REQUEST, info, name="user", format=format) -@bp.route("/create_user_form") -def create_user_form(): - raise NotImplementedError() +@bp.route("/create_user_form", methods=["GET", "POST"]) +@permission_required(Permission.ScoUsersAdmin) +@scodoc7func(context) +def create_user_form(context, REQUEST, user_name=None, edit=0): + "form. creation ou edit utilisateur" + auth_dept = current_user.dept + initvalues = {} + edit = int(edit) + H = [html_sco_header.sco_header(context, REQUEST, bodyOnLoad="init_tf_form('')")] + F = html_sco_header.sco_footer(context, REQUEST) + if edit: + if not user_name: + raise ValueError("missing argument: user_name") + initvalues = sco_users._user_list(user_name) + H.append("

Modification de l'utilisateur %s

" % user_name) + else: + H.append("

Création d'un utilisateur

") + + is_super_admin = False + if current_user.has_permission(Permission.ScoSuperAdmin, g.scodoc_dept): + H.append("""

Vous êtes super administrateur !

""") + is_super_admin = True + + # Noms de roles pouvant etre attribues aux utilisateurs via ce dialogue + # si pas SuperAdmin, restreint aux rôles EnsX, SecrX, AdminX + # + editable_roles = Role.query.all() + if is_super_admin: + log("create_user_form called by %s (super admin)" % (current_user.user_name,)) + else: + editable_roles = [ + r for r in editable_roles if r.name in {u"Ens", u"Secr", u"Admin"} + ] + # + if not edit: + submitlabel = "Créer utilisateur" + orig_roles = set() + else: + submitlabel = "Modifier utilisateur" + initvalues["roles"] = initvalues["roles"].split(",") or [] + orig_roles = set(initvalues["roles"]) + if initvalues["status"] == "old": + editable_roles = set() # can't change roles of a disabled user + # add existing user roles + displayed_roles = list(editable_roles.union(orig_roles)) + displayed_roles.sort() + disabled_roles = {} # pour desactiver les roles que l'on ne peut pas editer + for i in range(len(displayed_roles)): + if displayed_roles[i] not in editable_roles: + disabled_roles[i] = True + + # log('create_user_form: displayed_roles=%s' % displayed_roles) + + descr = [ + ("edit", {"input_type": "hidden", "default": edit}), + ("nom", {"title": "Nom", "size": 20, "allow_null": False}), + ("prenom", {"title": "Prénom", "size": 20, "allow_null": False}), + ] + if auth_name != user_name: # no one can't change its own status + descr.append( + ( + "status", + { + "title": "Statut", + "input_type": "radio", + "labels": ("actif", "ancien"), + "allowed_values": ("", "old"), + }, + ) + ) + if not edit: + descr += [ + ( + "user_name", + { + "title": "Pseudo (login)", + "size": 20, + "allow_null": False, + "explanation": "nom utilisé pour la connexion. Doit être unique parmi tous les utilisateurs.", + }, + ), + ( + "passwd", + { + "title": "Mot de passe", + "input_type": "password", + "size": 14, + "allow_null": False, + }, + ), + ( + "passwd2", + { + "title": "Confirmer mot de passe", + "input_type": "password", + "size": 14, + "allow_null": False, + }, + ), + ] + else: + descr += [ + ( + "user_name", + {"input_type": "hidden", "default": initvalues["user_name"]}, + ), + ("user_id", {"input_type": "hidden", "default": initvalues["user_id"]}), + ] + descr += [ + ( + "email", + { + "title": "e-mail", + "input_type": "text", + "explanation": "vivement recommandé: utilisé pour contacter l'utilisateur", + "size": 20, + "allow_null": True, + }, + ) + ] + + if not auth_dept: + # si auth n'a pas de departement (admin global) + # propose de choisir le dept du nouvel utilisateur + # sinon, il sera créé dans le même département que auth + descr.append( + ( + "dept", + { + "title": "Département", + "input_type": "text", + "size": 12, + "allow_null": True, + "explanation": """département d\'appartenance de l\'utilisateur (s'il s'agit d'un administrateur, laisser vide si vous voulez qu'il puisse créer des utilisateurs dans d'autres départements)""", + }, + ) + ) + can_choose_dept = True + else: + can_choose_dept = False + if edit: + descr.append( + ( + "d", + { + "input_type": "separator", + "title": "L'utilisateur appartient au département %s" + % auth_dept, + }, + ) + ) + else: + descr.append( + ( + "d", + { + "input_type": "separator", + "title": "L'utilisateur sera crée dans le département %s" + % auth_dept, + }, + ) + ) + + descr += [ + ( + "date_expiration", + { + "title": "Date d'expiration", # j/m/a + "input_type": "date", + "explanation": "j/m/a, laisser vide si pas de limite", + "size": 9, + "allow_null": True, + }, + ), + ( + "roles", + { + "title": "Rôles", + "input_type": "checkbox", + "vertical": True, + "allowed_values": displayed_roles, + "disabled_items": disabled_roles, + }, + ), + ( + "force", + { + "title": "Ignorer les avertissements", + "input_type": "checkbox", + "explanation": "passer outre les avertissements (homonymes, etc)", + "labels": ("",), + "allowed_values": ("1",), + }, + ), + ] + + if "tf-submitted" in REQUEST.form and not "roles" in REQUEST.form: + REQUEST.form["roles"] = [] + if "tf-submitted" in REQUEST.form: + # Ajoute roles existants mais non modifiables (disabled dans le form) + # orig_roles - editable_roles + REQUEST.form["roles"] = list( + set(REQUEST.form["roles"]).union(orig_roles - editable_roles) + ) + + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + descr, + initvalues=initvalues, + submitlabel=submitlabel, + cancelbutton="Annuler", + ) + if tf[0] == 0: + return "\n".join(H) + "\n" + tf[1] + F + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect(context.UsersURL()) + else: + vals = tf[2] + roles = set(vals["roles"]).intersection(editable_roles) + if REQUEST.form.has_key("edit"): + edit = int(REQUEST.form["edit"]) + else: + edit = 0 + try: + force = int(vals["force"][0]) + except: + force = 0 + + if edit: + user_name = initvalues["user_name"] + else: + user_name = vals["user_name"] + # ce login existe ? + err = None + users = _user_list(user_name) + if edit and not users: # safety net, le user_name ne devrait pas changer + err = "identifiant %s inexistant" % user_name + if not edit and users: + err = "identifiant %s déjà utilisé" % user_name + if err: + H.append(tf_error_message("""Erreur: %s""" % err)) + return "\n".join(H) + "\n" + tf[1] + F + + if not force: + ok, msg = context._check_modif_user( + edit, + user_name=user_name, + nom=vals["nom"], + prenom=vals["prenom"], + email=vals["email"], + roles=vals["roles"], + ) + if not ok: + H.append( + tf_error_message( + """Attention: %s (vous pouvez forcer l'opération en cochant "Ignorer les avertissements" en bas de page)""" + % msg + ) + ) + + return "\n".join(H) + "\n" + tf[1] + F + + if edit: # modif utilisateur (mais pas passwd) + if (not can_choose_dept) and vals.has_key("dept"): + del vals["dept"] + if vals.has_key("passwd"): + del vals["passwd"] + if vals.has_key("date_modif_passwd"): + del vals["date_modif_passwd"] + if vals.has_key("user_name"): + del vals["user_name"] + if (auth_name == user_name) and vals.has_key("status"): + del vals["status"] # no one can't change its own status + + # traitement des roles: ne doit pas affecter les roles + # que l'on en controle pas: + for role in orig_roles: + if role and not role in editable_roles: + roles.add(role) + + vals["roles"] = ",".join(roles) + + # ok, edit + log("sco_users: editing %s by %s" % (user_name, auth_name)) + # log('sco_users: previous_values=%s' % initvalues) + # log('sco_users: new_values=%s' % vals) + context._user_edit(user_name, vals) + return REQUEST.RESPONSE.redirect( + "userinfo?user_name=%s&head_message=Utilisateur %s modifié" + % (user_name, user_name) + ) + else: # creation utilisateur + vals["roles"] = ",".join(vals["roles"]) + # check identifiant + if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]): + msg = tf_error_message( + "identifiant invalide (pas d'accents ni de caractères spéciaux)" + ) + return "\n".join(H) + msg + "\n" + tf[1] + F + # check passwords + if vals["passwd"] != vals["passwd2"]: + msg = tf_error_message( + """Les deux mots de passes ne correspondent pas !""" + ) + return "\n".join(H) + msg + "\n" + tf[1] + F + if not sco_users.is_valid_password(vals["passwd"]): + msg = tf_error_message("""Mot de passe trop simple, recommencez !""") + return "\n".join(H) + msg + "\n" + tf[1] + F + if not can_choose_dept: + vals["dept"] = auth_dept + # ok, go + log("sco_users: new_user %s by %s" % (vals["user_name"], auth_name)) + context.create_user(vals, REQUEST=REQUEST) @bp.route("/import_users_form") @@ -90,6 +406,34 @@ def import_users_form(): @bp.route("/user_info_page") +@permission_required(Permission.ScoUsersView) @scodoc7func(context) def user_info_page(user_name, REQUEST=None): return sco_users.user_info_page(context, user_name=user_name, REQUEST=REQUEST) + + +@bp.route("/get_user_list_xml") +@permission_required(Permission.ScoView) +@scodoc7func(context) +def get_user_list_xml(context, dept=None, start="", limit=25, REQUEST=None): + """Returns XML list of users with name (nomplogin) starting with start. + Used for forms auto-completion.""" + userlist = sco_users.get_user_list(dept=dept) + start = scu.suppression_diacritics(unicode(start, "utf-8")) + start = scu.strlower(str(start)) + # TODO : à refaire avec une requete SQL #py3 + # (et en json) + userlist = [ + user + for user in userlist + if scu.suppress_accents(scu.strlower(user.nom)).startswith(start) + ] + if REQUEST: + REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) + doc = jaxml.XML_document(encoding=scu.SCO_ENCODING) + doc.results() + for user in userlist[:limit]: + doc._push() + doc.rs(user["nomplogin"], id=user["user_id"], info="") + doc._pop() + return repr(doc)