From d40a2b43cd419bc5c6ca81e04ebdb33fecdda3eb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Jun 2021 12:11:39 +0200 Subject: [PATCH] Page info utilisateur --- app/auth/models.py | 34 +- app/decorators.py | 2 +- app/scodoc/ZScoUsers.py | 1314 --------------------------------- app/scodoc/html_sco_header.py | 2 +- app/scodoc/html_sidebar.py | 3 +- app/scodoc/sco_permissions.py | 2 - app/scodoc/sco_users.py | 200 ++++- app/scodoc/sco_utils.py | 6 + app/views/users.py | 24 +- scodoc_manager.py | 2 +- 10 files changed, 227 insertions(+), 1362 deletions(-) delete mode 100644 app/scodoc/ZScoUsers.py diff --git a/app/auth/models.py b/app/auth/models.py index a769a592bd..e4ae2fc174 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -32,7 +32,7 @@ class User(UserMixin, db.Model): nom = db.Column(db.String(64)) prenom = db.Column(db.String(64)) dept = db.Column(db.String(32), index=True) - is_old = db.Column(db.Boolean, default=False, index=True) + active = db.Column(db.Boolean, default=True, index=True) password_hash = db.Column(db.String(128)) last_seen = db.Column(db.DateTime, default=datetime.utcnow) @@ -48,6 +48,7 @@ class User(UserMixin, db.Model): def __init__(self, **kwargs): self.roles = [] + self.user_roles = [] super(User, self).__init__(**kwargs) if ( not self.roles @@ -78,6 +79,8 @@ class User(UserMixin, db.Model): """Check given password vs current one. Returns `True` if the password matched, `False` otherwise. """ + if not self.active: # inactived users can't login + return False if not self.password_hash: # user without password can't login return False return check_password_hash(self.password_hash, password) @@ -99,30 +102,29 @@ class User(UserMixin, db.Model): return return User.query.get(id) - def to_dict(self, include_email=False): + def to_dict(self, include_email=True): data = { "date_expiration": self.date_expiration.isoformat() + "Z" if self.date_expiration - else None, + else "", "date_modif_passwd": self.date_modif_passwd.isoformat() + "Z" if self.date_modif_passwd - else None, + else "", "date_created": self.date_created.isoformat() + "Z" if self.date_created - else None, - "dept": self.dept, + else "", + "dept": (self.dept or "").encode("utf-8"), # sco8 "id": self.id, - "is_old": self.is_old, - "status_txt": "(ancien)" if self.is_old else "", + "active": self.active, + "status_txt": "actif" if self.active else "fermé", "last_seen": self.last_seen.isoformat() + "Z", - "nom": self.nom, - "prenom": self.prenom, - "roles": self.roles, + "nom": (self.nom or "").encode("utf-8"), # sco8 + "prenom": (self.prenom or "").encode("utf-8"), # sco8 "roles_string": self.get_roles_string(), - "user_name": self.user_name, + "user_name": self.user_name.encode("utf-8"), # sco8 } if include_email: - data["email"] = self.email + data["email"] = self.email or "" return data def from_dict(self, data, new_user=False): @@ -154,12 +156,14 @@ class User(UserMixin, db.Model): # Permissions management: def has_permission(self, perm, dept=False): """Check if user has permission `perm` in given `dept`. - Emulate Zope `has_permission`` + Similar to Zope ScoDoc7 `has_permission`` Args: perm: integer, one of the value defined in Permission class. dept: dept id (eg 'RT') """ + if not self.active: + return False if dept is False: dept = g.scodoc_dept # les role liés à ce département, et les roles avec dept=None (super-admin) @@ -201,7 +205,7 @@ class User(UserMixin, db.Model): return ", ".join("{r.role.name}{r.dept}".format(r=r) for r in self.user_roles) def is_administrator(self): - return self.has_permission(Permission.ScoSuperAdmin, None) + return self.active and self.has_permission(Permission.ScoSuperAdmin, None) class AnonymousUser(AnonymousUserMixin): diff --git a/app/decorators.py b/app/decorators.py index 720bd4e755..0fe4f46c3e 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -142,7 +142,7 @@ def scodoc7func(context): return func(*args, **kwargs) # if "scodoc_dept" in kwargs: - g.scodoc_dept = kwargs["scodoc_dept"] + g.scodoc_dept = kwargs["scodoc_dept"].encode("utf-8") # sco8 del kwargs["scodoc_dept"] elif not hasattr(g, "scodoc_dept"): g.scodoc_dept = None diff --git a/app/scodoc/ZScoUsers.py b/app/scodoc/ZScoUsers.py deleted file mode 100644 index a8418ed47d..0000000000 --- a/app/scodoc/ZScoUsers.py +++ /dev/null @@ -1,1314 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Emmanuel Viennet emmanuel.viennet@viennet.net -# -############################################################################## - -""" Gestion des utilisateurs (table SQL pour Zope User Folder) -""" -import string -import re -import time -import md5 -import base64 -import jaxml - -from app.scodoc.sco_zope import * # pylint: disable=unused-wildcard-import - -# --------------- -import app.scodoc.sco_utils as scu -import app.scodoc.notesdb as ndb -from app.scodoc.notes_log import log -from app.scodoc.scolog import logdb -from scolars import format_prenom, format_nom -from app.scodoc import sco_import_users -from app.scodoc import sco_excel -from app.scodoc.TrivialFormulator import TrivialFormulator, TF, tf_error_message -from app.scodoc.gen_tables import GenTable -from app.scodoc import sco_etud -from app.scodoc import sco_cache -from app.scodoc import sco_users - -from app.scodoc.sco_exceptions import ( - AccessDenied, - ScoException, - ScoValueError, - ScoInvalidDateError, - ScoLockedFormError, - ScoGenError, -) - -# --------------- -# cache global: chaque instance, repérée par son URL, a un cache -# qui est recréé à la demande -# On cache ici la liste des utilisateurs, pour une duree limitee -# (une minute). - -CACHE_userlist = {} - -# --------------- - - -class ZScoUsers( - ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit -): - - "ZScousers object" - - meta_type = "ZScoUsers" - security = ClassSecurityInfo() - - # This is the list of the methods associated to 'tabs' in the ZMI - # Be aware that The first in the list is the one shown by default, so if - # the 'View' tab is the first, you will never see your tabs by cliquing - # on the object. - manage_options = ( - ({"label": "Contents", "action": "manage_main"},) - + PropertyManager.manage_options # add the 'Properties' tab - + ({"label": "View", "action": "index_html"},) - + Item.manage_options # add the 'Undo' & 'Owner' tab - + RoleManager.manage_options # add the 'Security' tab - ) - - # no permissions, only called from python - def __init__(self, id, title): - "initialise a new instance" - self.id = id - self.title = title - - # Connexion to SQL database of users: - - # Ugly but necessary during transition out of Zope: - _db_cnx_string = "dbname=SCOUSERS port=5432" - security.declareProtected("Change DTML Documents", "GetUsersDBConnexion") - GetUsersDBConnexion = ndb.GetUsersDBConnexion - - # -------------------------------------------------------------------- - # - # Users (top level) - # - # -------------------------------------------------------------------- - # used to view content of the object - security.declareProtected(ScoUsersView, "index_html") - - def index_html(self, REQUEST, all=0, with_olds=0, format="html"): - "gestion utilisateurs..." - all = int(all) - with_olds = int(with_olds) - # Controle d'acces - authuser = REQUEST.AUTHENTICATED_USER - user_name = str(authuser) - # log('user: %s roles: %s'%(user_name,authuser.getRolesInContext(self))) - user = self._user_list(args={"user_name": user_name}) - if not user: - zope_roles = authuser.getRolesInContext(self) - if ("Manager" in zope_roles) or ("manage" in zope_roles): - dept = "" # special case for zope admin - else: - raise AccessDenied("Vous n'avez pas la permission de voir cette page") - else: - dept = user[0]["dept"] - - H = [self.sco_header(REQUEST, page_title="Gestion des utilisateurs")] - H.append("

Gestion des utilisateurs

") - - if authuser.has_permission(Permission.ScoUsersAdmin, self): - H.append( - '

Ajouter un utilisateur' - ) - H.append( - '   Importer des utilisateurs

' - ) - if all: - checked = "checked" - else: - checked = "" - if with_olds: - olds_checked = "checked" - else: - olds_checked = "" - H.append( - """

- Tous les départements - Avec anciens utilisateurs -

""" - % (REQUEST.URL0, checked, olds_checked) - ) - - L = self.list_users( - dept, - all=all, - with_olds=with_olds, - format=format, - REQUEST=REQUEST, - with_links=authuser.has_permission(Permission.ScoUsersAdmin, self), - ) - if format != "html": - return L - H.append(L) - - F = self.sco_footer(REQUEST) - return "\n".join(H) + F - - _userEditor = ndb.EditableTable( - "sco_users", - "user_id", - ( - "user_id", - "user_name", - "passwd", - "roles", - "date_modif_passwd", - "nom", - "prenom", - "email", - "dept", - "passwd_temp", - "status", - "date_expiration", - ), - output_formators={ - "date_modif_passwd": ndb.DateISOtoDMY, - "date_expiration": ndb.DateISOtoDMY, - }, - input_formators={ - "date_modif_passwd": ndb.DateDMYtoISO, - "date_expiration": ndb.DateDMYtoISO, - }, - sortkey="nom", - filter_nulls=False, - ) - - def _user_list(self, **kw): - # list info sur utilisateur(s) - cnx = self.GetUsersDBConnexion() - users = self._userEditor.list(cnx, **kw) - for u in users: - if u["status"] == "old": - u["status_txt"] = "(ancien)" - else: - u["status_txt"] = "" - - return users - - def _user_edit(self, user_name, vals): - # edit user - cnx = self.GetUsersDBConnexion() - vals["user_name"] = user_name - self._userEditor.edit(cnx, vals) - self.get_userlist_cache().inval_cache() # > - self.acl_users.cache_removeUser(user_name) # exUserFolder's caches - self.acl_users.xcache_removeUser(user_name) - # Ensure that if status is "old", login is disabled - # note that operation is reversible without having to re-enter a password - # We change the roles (to avoid dealing with passwd hash, controled by exUserFolder) - u = self._user_list(args={"user_name": user_name})[0] - if u["status"] == "old" and u["roles"] and u["roles"][0] != "-": - roles = ["-" + r for r in u["roles"].split(",")] - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - self.acl_users.scodoc_editUser(cursor, user_name, roles=roles) - self.get_userlist_cache().inval_cache() - elif not u["status"] and u["roles"] and u["roles"][0] == "-": - roles = [r[1:] for r in u["roles"].split(",") if (r and r[0] == "-")] - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - self.acl_users.scodoc_editUser(cursor, user_name, roles=roles) - self.get_userlist_cache().inval_cache() - - def _user_delete(self, user_name): - # delete user - cnx = self.GetUsersDBConnexion() - user_id = self._user_list(args={"user_name": user_name})[0]["user_id"] - self._userEditor.delete(cnx, user_id) - self.get_userlist_cache().inval_cache() # > - - def _all_roles(self): - "ensemble de tous les roles attribués ou attribuables" - roles = set(self.DeptUsersRoles()) - cnx = self.GetUsersDBConnexion() - L = self._userEditor.list(cnx, {}) - for l in L: - roles.update([x.strip() for x in l["roles"].split(",")]) - return [r for r in roles if r and r[0] != "-"] - - security.declareProtected(ScoUsersAdmin, "user_info") - - def user_info(self, user_name=None, user=None, format=None, REQUEST=None): - """Donne 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: - infos = self._user_list(args={"user_name": user_name}) - else: - infos = [user.copy()] - user_name = user["user_name"] - - if not infos: - # special case: user is not in our database - return { - "user_name": user_name, - "nom": user_name, - "prenom": "", - "email": "", - "dept": "", - "nomprenom": user_name, - "prenomnom": user_name, - "prenom_fmt": "", - "nom_fmt": user_name, - "nomcomplet": user_name, - "nomplogin": user_name, - "nomnoacc": scu.suppress_accents(user_name), - "passwd_temp": 0, - "status": "", - "date_expiration": None, - } - else: - info = infos[0] - # always conceal password ! - del info["passwd"] # always conceal password ! - # - if info["prenom"]: - p = format_prenom(info["prenom"]) - else: - p = "" - if info["nom"]: - n = format_nom( - info["nom"], uppercase=False - ) # strcapitalize(strlower(info['nom'])) - else: - n = user_name - - prenom_abbrv = scu.abbrev_prenom(p) - # nomprenom est le nom capitalisé suivi de l'initiale du prénom - info["nomprenom"] = (n + " " + prenom_abbrv).strip() - # prenomnom est l'initiale du prénom suivie du nom - info["prenomnom"] = (prenom_abbrv + " " + n).strip() - # nom_fmt et prenom_fmt: minuscule capitalisé - info["nom_fmt"] = n - info["prenom_fmt"] = sco_etud.format_prenom(p) - # nomcomplet est le prenom et le nom complets - info["nomcomplet"] = info["prenom_fmt"] + " " + info["nom_fmt"] - # nomplogin est le nom en majuscules suivi du prénom et du login - # e.g. Dupont Pierre (dupont) - info["nomplogin"] = "%s %s (%s)" % (scu.strupper(n), p, info["user_name"]) - # nomnoacc est le nom en minuscules sans accents - info["nomnoacc"] = scu.suppress_accents(scu.strlower(info["nom"])) - - return scu.sendResult(REQUEST, info, name="user", format=format) - - def _can_handle_passwd(self, authuser, user_name, allow_admindepts=False): - """true if authuser can see or change passwd of user_name. - If allow_admindepts, allow Admin from all depts (so they can view users from other depts - and add roles to them). - authuser is a Zope user object. user_name is a string. - """ - # Is authuser a zope admin ? - zope_roles = authuser.getRolesInContext(self) - if ("Manager" in zope_roles) or ("manage" in zope_roles): - return True - # Anyone can change its own passwd (or see its informations) - if str(authuser) == user_name: - return True - # has permission ? - if not authuser.has_permission(Permission.ScoUsersAdmin, self): - return False - # Ok, now check that authuser can manage users from this departement - # Get user info - user = self._user_list(args={"user_name": user_name}) - if not user: - return False # we don't have infos on this user ! - # Get authuser info - auth_name = str(authuser) - authuser_info = self._user_list(args={"user_name": auth_name}) - if not authuser_info: - return False # not admin, and not in out database - auth_dept = authuser_info[0]["dept"] - if not auth_dept: - return True # if no dept, can access users from all depts ! - if auth_dept == user[0]["dept"] or allow_admindepts: - return True - else: - return False - - def do_change_password(self, user_name, password): - user = self._user_list(args={"user_name": user_name}) - assert len(user) == 1, "database inconsistency: len(user)=%d" % len(user) - # should not occur, already tested in _can_handle_passwd - cnx = self.GetUsersDBConnexion() # en mode autocommit - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - "update sco_users set date_modif_passwd=now(), passwd_temp=0 where user_name=%(user_name)s", - {"user_name": user_name}, - ) - - # Laisse le exUserFolder modifier les donnees: - self.acl_users.scodoc_editUser( - cursor, user_name, password=password, roles=[user[0]["roles"]] - ) - - log("change_password: change ok for %s" % user_name) - self.get_userlist_cache().inval_cache() # > - - security.declareProtected(ScoView, "change_password") - - def change_password(self, user_name, password, password2, REQUEST): - "change a password" - # ScoUsersAdmin: modif tous les passwd de SON DEPARTEMENT - # sauf si pas de dept (admin global) - H = [] - F = self.sco_footer(REQUEST) - # Check access permission - if not self._can_handle_passwd(REQUEST.AUTHENTICATED_USER, user_name): - # access denied - log( - "change_password: access denied (authuser=%s, user_name=%s, ip=%s)" - % (REQUEST.AUTHENTICATED_USER, user_name, REQUEST.REMOTE_ADDR) - ) - raise AccessDenied( - "vous n'avez pas la permission de changer ce mot de passe" - ) - # check password - if password != password2: - H.append( - """

Les deux mots de passes saisis sont différents !

-

Recommencer

""" - % user_name - ) - else: - if not sco_users.is_valid_password(password): - H.append( - """

ce mot de passe n\'est pas assez compliqué !
(oui, il faut un mot de passe vraiment compliqué !)

-

Recommencer

- """ - % user_name - ) - else: - # ok, strong password - # MD5 hash (now computed by exUserFolder) - # digest = md5.new() - # digest.update(password) - # digest = digest.digest() - # md5pwd = string.strip(base64.encodestring(digest)) - # - self.do_change_password(user_name, password) - # - # ici page simplifiee car on peut ne plus avoir - # le droit d'acceder aux feuilles de style - H.append( - "

Changement effectué !

Ne notez pas ce mot de passe, mais mémorisez le !

Rappel: il est interdit de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !

Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.

" - ) - return ( - """ - - - -Mot de passe changé - -

Mot de passe changé !

-""" - % (scu.SCO_ENCODING, scu.SCO_ENCODING) - + "\n".join(H) - + 'Continuer' - % self.ScoURL() - ) - return self.sco_header(REQUEST) + "\n".join(H) + F - - security.declareProtected(ScoView, "form_change_password") - - def form_change_password(self, REQUEST, user_name=None): - """Formulaire changement mot de passe - Un utilisateur peut toujours changer son mot de passe""" - authuser = REQUEST.AUTHENTICATED_USER - if not user_name: - user_name = str(authuser) - H = [self.sco_header(REQUEST, user_check=False)] - F = self.sco_footer(REQUEST) - # check access - if not self._can_handle_passwd(authuser, user_name): - return ( - "\n".join(H) - + "

Vous n'avez pas la permission de changer ce mot de passe

" - + F - ) - # - H.append( - """

Changement du mot de passe de %(user_name)s

-

-

- - -
Nouveau mot de passe:
Confirmation:
- - -

-

Vous pouvez aussi: renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur -""" - % {"user_name": user_name, "url": REQUEST.URL0} - ) - return "\n".join(H) + F - - security.declareProtected(ScoView, "userinfo") - - def userinfo(self, user_name=None, REQUEST=None): - "display page of info about connected user" - authuser = REQUEST.AUTHENTICATED_USER - if not user_name: - user_name = str(authuser) - # peut on divulguer ces infos ? - if not self._can_handle_passwd( - REQUEST.AUTHENTICATED_USER, user_name, allow_admindepts=True - ): - raise AccessDenied("Vous n'avez pas la permission de voir cette page") - H = [self.sco_header(REQUEST, page_title="Utilisateur %s" % user_name)] - F = self.sco_footer(REQUEST) - H.append("

Utilisateur: %s" % user_name) - info = self._user_list(args={"user_name": user_name}) - if info: - H.append("%(status_txt)s" % info[0]) - H.append("

") - if not info: - H.append( - "

L' utilisateur '%s' n'est pas défini dans ce module.

" - % user_name - ) - if authuser.has_permission(Permission.ScoEditAllNotes, self): - H.append("

(il peut modifier toutes les notes)

") - if authuser.has_permission(Permission.ScoEditAllEvals, self): - H.append("

(il peut modifier toutes les évaluations)

") - if authuser.has_permission(Permission.ScoImplement, self): - H.append("

(il peut creer des formations)

") - else: - H.append( - """

- Login : %(user_name)s
- Nom : %(nom)s
- Prénom : %(prenom)s
- Mail : %(email)s
- Roles : %(roles)s
- Dept : %(dept)s
- Dernière modif mot de passe: %(date_modif_passwd)s
- Date d'expiration: %(date_expiration)s -

") - - if str(authuser) == user_name: - H.append( - '

Se déconnecter: logout

' - ) - # Liste des permissions - H.append( - '

Permission de cet utilisateur:

    ' - ) - permissions = self.ac_inherited_permissions(1) - scoperms = [p for p in permissions if p[0][:3] == "Sco"] - try: - thisuser = self.acl_users.getUser(user_name) - except: - # expired from cache ? retry... - thisuser = self.acl_users.getUser(user_name) - if not thisuser: - # Cas de figure incompris ? (bug IUT Amiens janvier 2014: login avec accent ?) - H.append( - "
  • impossible de retrouver les permissions de l'utilisateur (contacter l'administrateur)
  • " - ) - else: - for p in scoperms: - permname, _ = p[:2] - if thisuser.has_permission(permname, self): - b = "oui" - else: - b = "non" - H.append("
  • %s : %s
  • " % (permname, b)) - H.append("
") - if authuser.has_permission(Permission.ScoUsersAdmin, self): - H.append( - '

Liste de tous les utilisateurs

' - % self.ScoURL() - ) - return "\n".join(H) + F - - security.declareProtected(ScoUsersAdmin, "create_user_form") - - def create_user_form(self, REQUEST, user_name=None, edit=0): - "form. creation ou edit utilisateur" - # Get authuser info - authuser = REQUEST.AUTHENTICATED_USER - auth_name = str(authuser) - authuser_info = self._user_list(args={"user_name": auth_name}) - initvalues = {} - - # Access control - zope_roles = authuser.getRolesInContext(self) - if ( - not authuser_info - and not ("Manager" in zope_roles) - and not ("manage" 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 = "" - # - edit = int(edit) - H = [self.sco_header(REQUEST, bodyOnLoad="init_tf_form('')")] - F = self.sco_footer(REQUEST) - if edit: - if not user_name: - raise ValueError("missing argument: user_name") - initvalues = self._user_list(args={"user_name": user_name})[0] - H.append("

Modification de l'utilisateur %s

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

Création d'un utilisateur

") - - if authuser.has_permission(Permission.ScoSuperAdmin, self): - H.append("""

Vous êtes super administrateur !

""") - - # Noms de roles pouvant etre attribues aux utilisateurs via ce dialogue - # si pas SuperAdmin, restreint aux rôles EnsX, SecrX, DeptX - # - if authuser.has_permission(Permission.ScoSuperAdmin, self): - log("create_user_form called by %s (super admin)" % (auth_name,)) - editable_roles = set(self._all_roles()) - else: - editable_roles = set(self.DeptUsersRoles()) - # log('create_user_form: editable_roles=%s' % editable_roles) - # - 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(self.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 = self._user_list(args={"user_name": 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 = self._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) - self._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)) - self.create_user(vals, REQUEST=REQUEST) - - def _check_modif_user( - self, edit, user_name="", nom="", prenom="", email="", roles=[] - ): - """Vérifie que et utilisateur peut etre crée (edit=0) ou modifié (edit=1) - Cherche homonymes. - returns (ok, msg) - - ok : si vrai, peut continuer avec ces parametres - (si ok est faux, l'utilisateur peut quand même forcer la creation) - - msg: message warning a presenter l'utilisateur - """ - if not user_name or not nom or not prenom: - return False, "champ requis vide" - if not email: - return False, "vous devriez indiquer le mail de l'utilisateur créé !" - # ce login existe ? - users = self._user_list(args={"user_name": user_name}) - if edit and not users: # safety net, le user_name ne devrait pas changer - return False, "identifiant %s inexistant" % user_name - if not edit and users: - return False, "identifiant %s déjà utilisé" % user_name - - # Des noms/prénoms semblables existent ? - cnx = self.GetUsersDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - "select * from sco_users where lower(nom) ~ %(nom)s and lower(prenom) ~ %(prenom)s;", - {"nom": nom.lower().strip(), "prenom": prenom.lower().strip()}, - ) - res = cursor.dictfetchall() - if edit: - minmatch = 1 - else: - minmatch = 0 - if len(res) > minmatch: - return ( - False, - "des utilisateurs proches existent: " - + ", ".join( - [ - "%s %s (pseudo=%s)" % (x["prenom"], x["nom"], x["user_name"]) - for x in res - ] - ), - ) - # Roles ? - if not roles and not (edit and users[0]["status"] == "old"): - # nb: si utilisateur desactivé (old), pas de role attribué - return False, "aucun rôle sélectionné, êtes vous sûr ?" - # ok - return True, "" - - security.declareProtected(ScoUsersAdmin, "import_users_form") - - def import_users_form(self, REQUEST, user_name=None, edit=0): - """Import utilisateurs depuis feuille Excel""" - head = self.sco_header(REQUEST, 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: -

-
    -
  1. vérification des données;
  2. -
  3. génération d'un mot de passe alétoire pour chaque utilisateur;
  4. -
  5. création de chaque utilisateur;
  6. -
  7. envoi à chaque utilisateur de son mot de passe initial par mail.
  8. -
""" - H.append( - """
  1. - Obtenir la feuille excel à remplir
  2. """ - ) - F = self.sco_footer(REQUEST) - 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 REQUEST.RESPONSE.redirect(self.UsersURL()) - else: - # IMPORT - diag = sco_import_users.import_excel_file( - tf[2]["xlsfile"], REQUEST=REQUEST, context=self - ) - H = [head] - H.append("

Import excel: %s

" % diag) - H.append("

OK, import terminé !

") - H.append( - '

Continuer

' % self.UsersURL() - ) - return "\n".join(H) + help + F - - security.declareProtected(ScoUsersAdmin, "import_users_generate_excel_sample") - - def import_users_generate_excel_sample(self, REQUEST): - "une feuille excel pour importation utilisateurs" - data = sco_import_users.generate_excel_sample() - return sco_excel.sendExcelFile(REQUEST, data, "ImportUtilisateurs.xls") - - security.declareProtected(ScoUsersAdmin, "create_user") - - def create_user(self, args, REQUEST=None): - "creation utilisateur zope" - cnx = self.GetUsersDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - passwd = args["passwd"] - args["passwd"] = "undefined" - if "passwd2" in args: - del args["passwd2"] - log("create_user: args=%s" % args) # log apres supr. du mot de passe ! - _ = self._userEditor.create(cnx, args) - self.get_userlist_cache().inval_cache() # > - - # call exUserFolder to set passwd - roles = args["roles"].split(",") - self.acl_users.scodoc_editUser( - cursor, args["user_name"], password=passwd, roles=roles - ) - - if REQUEST: - return REQUEST.RESPONSE.redirect(self.UsersURL()) - - security.declareProtected(ScoUsersAdmin, "delete_user_form") - - def delete_user_form(self, REQUEST, user_name, dialog_confirmed=False): - "delete user" - authuser = REQUEST.AUTHENTICATED_USER - if not self._can_handle_passwd(authuser, user_name): - return ( - self.sco_header(REQUEST, user_check=False) - + "

Vous n'avez pas la permission de supprimer cet utilisateur

" - + self.sco_footer(REQUEST) - ) - - r = self._user_list(args={"user_name": user_name}) - if len(r) != 1: - return ScoValueError("utilisateur %s inexistant" % user_name) - if not dialog_confirmed: - return self.confirmDialog( - """

Confirmer la suppression de l\'utilisateur %s ?

-

En général, il est déconseillé de supprimer un utilisateur, son - identité étant référencé dans les modules de formation. N'utilisez - cette fonction qu'en cas d'erreur (création de doublons, etc). -

- """ - % user_name, - dest_url="", - REQUEST=REQUEST, - cancel_url=self.UsersURL(), - parameters={"user_name": user_name}, - ) - self._user_delete(user_name) - return REQUEST.RESPONSE.redirect( - self.UsersURL() + r"?head_message=Utilisateur%20supprimé" - ) - - security.declareProtected(ScoView, "list_users") - - def list_users( - self, - dept, - all=False, # tous les departements - with_olds=False, # inclue les anciens utilisateurs (status "old") - format="html", - with_links=True, - REQUEST=None, - ): - "List users" - authuser = REQUEST.AUTHENTICATED_USER - if dept and not all: - r = self.get_userlist(dept=dept, with_olds=with_olds) - comm = "dept. %s" % dept - else: - r = self.get_userlist(with_olds=with_olds) - comm = "tous" - if with_olds: - comm += ", avec anciens" - comm = "(" + comm + ")" - # -- Add some information and links: - for u in r: - # Can current user modify this user ? - can_modify = self._can_handle_passwd( - authuser, u["user_name"], allow_admindepts=True - ) - - # Add links - if with_links and can_modify: - target = "userinfo?user_name=%(user_name)s" % u - u["_user_name_target"] = target - u["_nom_target"] = target - u["_prenom_target"] = target - - # Hide passwd modification date (depending on rights wrt user) - if not can_modify: - u["date_modif_passwd"] = "(non visible)" - - # Add spaces between roles to ease line wrap - if u["roles"]: - u["roles"] = ", ".join(u["roles"].split(",")) - - # Convert dates to ISO if XML output - if format == "xml" and u["date_modif_passwd"] != "NA": - u["date_modif_passwd"] = ndb.DateDMYtoISO(u["date_modif_passwd"]) or "" - - # Convert date_expiration and date_modif_passwd to ISO to ease sorting - if u["date_expiration"]: - u["date_expiration_iso"] = ndb.DateDMYtoISO(u["date_expiration"]) - else: - u["date_expiration_iso"] = "" - if u["date_modif_passwd"]: - u["date_modif_passwd_iso"] = ndb.DateDMYtoISO(u["date_expiration"]) - else: - u["date_modif_passwd_iso"] = "" - - title = "Utilisateurs définis dans ScoDoc" - tab = GenTable( - rows=r, - columns_ids=( - "user_name", - "nom_fmt", - "prenom_fmt", - "email", - "dept", - "roles", - "date_expiration_iso", - "date_modif_passwd_iso", - "passwd_temp", - "status_txt", - ), - titles={ - "user_name": "Login", - "nom_fmt": "Nom", - "prenom_fmt": "Prénom", - "email": "Mail", - "dept": "Dept.", - "roles": "Rôles", - "date_expiration_iso": "Expiration", - "date_modif_passwd_iso": "Modif. mot de passe", - "passwd_temp": "Temp.", - "status_txt": "Etat", - }, - caption=title, - page_title="title", - html_title="""

%d utilisateurs %s

-

Cliquer sur un nom pour changer son mot de passe

""" - % (len(r), comm), - html_class="table_leftalign list_users", - html_with_td_classes=True, - html_sortable=True, - base_url="%s?all=%s" % (REQUEST.URL0, all), - pdf_link=False, # table is too wide to fit in a paper page => disable pdf - preferences=self.get_preferences(), - ) - - return tab.make_page( - self, format=format, with_html_headers=False, REQUEST=REQUEST - ) - - def get_userlist_cache(self): - url = self.ScoURL() - if CACHE_userlist.has_key(url): - return CACHE_userlist[url] - else: - log("get_userlist_cache: new simpleCache") - CACHE_userlist[url] = sco_cache.expiringCache(max_validity=60) - return CACHE_userlist[url] - - security.declareProtected(ScoView, "get_userlist") - - def get_userlist(self, dept=None, with_olds=False): - """Returns list of users. - If dept, select users from this dept, - else return all users. - """ - # on ne cache que la liste sans les "olds" - if with_olds: - r = None - else: - cache = self.get_userlist_cache() - r = cache.get(dept) - - if r != None: - return r - else: - args = {} - if not with_olds: - args["status"] = None - if dept != None: - args["dept"] = dept - - r = self._user_list(args=args) - - l = [self.user_info(user=user) for user in r] - - if not with_olds: - cache.set(dept, l) - return l - - security.declareProtected(ScoView, "get_userlist_xml") - - def get_userlist_xml(self, dept=None, start="", limit=25, REQUEST=None): - """Returns XML list of users with name (nomplogin) starting with start. - Used for forms auto-completion.""" - userlist = self.get_userlist(dept=dept) - start = scu.suppression_diacritics(unicode(start, "utf-8")) - start = scu.strlower(str(start)) - - userlist = [user for user in userlist if user["nomnoacc"].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) - - security.declareProtected(ScoView, "get_user_name_from_nomplogin") - - def get_user_name_from_nomplogin(self, nomplogin): - """Returns user_name (login) from nomplogin""" - m = re.match(r".*\((.*)\)", nomplogin.strip()) - if m: - return m.group(1) - else: - return None - - security.declareProtected(ScoView, "reset_password_form") - - def reset_password_form(self, user_name=None, dialog_confirmed=False, REQUEST=None): - """Form to reset a password""" - if not dialog_confirmed: - return self.confirmDialog( - """

Ré-initialiser le mot de passe de %s ?

-

Le mot de passe de %s va être choisi au hasard et lui être envoyé par mail. -Il devra ensuite se connecter et le changer. -

- """ - % (user_name, user_name), - parameters={"user_name": user_name}, - REQUEST=REQUEST, - ) - self.reset_password(user_name=user_name, REQUEST=REQUEST) - return REQUEST.RESPONSE.redirect( - self.UsersURL() - + r"?head_message=mot%20de%20passe%20de%20" - + user_name - + "%20reinitialise" - ) - - security.declareProtected(ScoView, "reset_password") - - def reset_password(self, user_name=None, REQUEST=None): - """Reset a password: - - set user's passwd_temp to 1 - - set roles to 'ScoReset' - - generate a random password and mail it - """ - authuser = REQUEST.AUTHENTICATED_USER - auth_name = str(authuser) - if not user_name: - user_name = auth_name - # Access control - if not self._can_handle_passwd(authuser, user_name): - raise AccessDenied( - "vous n'avez pas la permission de changer ce mot de passe" - ) - log("reset_password: %s" % user_name) - # Check that user has valid mail - info = self.user_info(user_name=user_name) - if not scu.is_valid_mail(info["email"]): - raise Exception("pas de mail valide associé à l'utilisateur") - # Generate random password - password = sco_import_users.generate_password() - self.do_change_password(user_name, password) - # Flag it as temporary: - cnx = self.GetUsersDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - ui = {"user_name": user_name} - cursor.execute( - "update sco_users set passwd_temp=1 where user_name='%(user_name)s'" % ui - ) - # Send email - info["passwd"] = password - sco_import_users.mail_password(info, context=self, reset=True) - - -# -------------------------------------------------------------------- -# -# Zope Product Administration -# -# -------------------------------------------------------------------- -def manage_addZScoUsers( - self, id="id_ZScousers", title="The Title for ZScoUsers Object", REQUEST=None -): - "Add a ZScoUsers instance to a folder." - self._setObject(id, ZScoUsers(id, title)) - if REQUEST is not None: - return self.manage_main(self, REQUEST) diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 72fb42d8e8..991fa6fa0b 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -334,7 +334,7 @@ def sco_header( # div pour affichage messages temporaires H.append('
') # - return "".join(H) + return scu.sco8_join(H, sep="") def sco_footer(context, REQUEST=None): diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 4083a75d9c..cc48c167a5 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -42,11 +42,12 @@ def sidebar_common(context, REQUEST=None): "UsersURL": scu.UsersURL(), "NotesURL": scu.NotesURL(), "AbsencesURL": scu.AbsencesURL(), + # XXX TODO "LogoutURL" : , "authuser": str(authuser), } H = [ 'ScoDoc', - '' + '' % params, sidebar_dept(context, REQUEST), """

Scolarité

diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 0336fc3246..bc8a7864dc 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -50,8 +50,6 @@ class Permission: def init_permissions(): for (perm, symbol, description) in _SCO_PERMISSIONS: setattr(Permission, symbol, perm) - # Crée aussi les attributs dans le module (ScoDoc7 compat) - globals()[symbol] = perm Permission.description[symbol] = description Permission.NBITS = len(_SCO_PERMISSIONS) diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index ac422791a1..5a8235a3bf 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -30,12 +30,12 @@ # Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy -import string -import re -import time -import md5 import base64 import jaxml +import md5 +import re +import string +import time from flask import current_app, url_for, g from flask_login import current_user @@ -94,10 +94,10 @@ def is_valid_password(cleartxt): # XXX TODO supprimer ndb.GetUsersDBConnexion -def index_html(context, REQUEST, all_depts=False, with_olds=False, format="html"): +def index_html(context, REQUEST, all_depts=False, with_inactives=False, format="html"): "gestion utilisateurs..." all_depts = int(all_depts) - with_olds = int(with_olds) + with_inactives = int(with_inactives) H = [html_sco_header.html_sem_header(context, REQUEST, "Gestion des utilisateurs")] H.append("

Gestion des utilisateurs

") @@ -121,14 +121,14 @@ def index_html(context, REQUEST, all_depts=False, with_olds=False, format="html" checked = "checked" else: checked = "" - if with_olds: + if with_inactives: olds_checked = "checked" else: olds_checked = "" H.append( """

Tous les départements - Avec anciens utilisateurs + Avec anciens utilisateurs

""" % (REQUEST.URL0, checked, olds_checked) ) @@ -137,7 +137,7 @@ def index_html(context, REQUEST, all_depts=False, with_olds=False, format="html" context, g.scodoc_dept, all_depts=all_depts, - with_olds=with_olds, + with_inactives=with_inactives, format=format, REQUEST=REQUEST, with_links=current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept), @@ -154,19 +154,19 @@ def list_users( context, dept, all_depts=False, # tous les departements - with_olds=False, # inclut les anciens utilisateurs (status "old") + with_inactives=False, # inclut les anciens utilisateurs (status "old") format="html", with_links=True, REQUEST=None, ): "List users, returns a table in the specified format" if dept and not all_depts: - users = get_user_list(dept=dept, with_olds=with_olds) + users = get_user_list(dept=dept, with_inactives=with_inactives) comm = "dept. %s" % dept.encode(scu.SCO_ENCODING) # sco8 else: - r = get_user_list(with_olds=with_olds) + r = get_user_list(with_inactives=with_inactives) comm = "tous" - if with_olds: + if with_inactives: comm += ", avec anciens" comm = "(" + comm + ")" # -- Add some information and links: @@ -180,7 +180,7 @@ def list_users( # Add links if with_links and can_modify: target = url_for( - "users.userinfo", scodoc_dept=g.scodoc_dept, user_name=u.user_name + "users.user_info_page", scodoc_dept=dept, user_name=u.user_name ).encode( scu.SCO_ENCODING ) # sco8 @@ -237,7 +237,7 @@ def list_users( ) -def get_user_list(dept=None, with_olds=False): +def get_user_list(dept=None, with_inactives=False): """Returns list of users. If dept, select users from this dept, else return all users. @@ -246,10 +246,170 @@ def get_user_list(dept=None, with_olds=False): q = User.query if dept is not None: q = q.filter_by(dept=dept) - if not with_olds: - q = q.filter_by(is_old=False) - return q.all() + if not with_inactives: + q = q.filter_by(active=True) + return q.order_by(User.nom, User.user_name).all() -# def get_user_infos(user_list): -# return [ user_info(u) for u in user_list ] +def _user_list(user_name): + "return user as a dict" + u = User.query.filter_by(user_name=user_name).first() + if u: + return u.to_dict() + else: + return None + + +def user_info(user_name=None, user=None): + """Donne 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: + info = _user_list(user_name) + else: + info = [user.copy()] + user_name = user["user_name"] + + if not info: + # special case: user is not in our database + return { + "user_name": user_name, + "nom": user_name, + "prenom": "", + "email": "", + "dept": "", + "nomprenom": user_name, + "prenomnom": user_name, + "prenom_fmt": "", + "nom_fmt": user_name, + "nomcomplet": user_name, + "nomplogin": user_name, + "nomnoacc": scu.suppress_accents(user_name), + "passwd_temp": 0, + "status": "", + "date_expiration": None, + } + else: + # Ensure we never publish password hash + if "password_hash" in info: + del info["password_hash"] + # + if info["prenom"]: + p = format_prenom(info["prenom"]) + else: + p = "" + if info["nom"]: + n = format_nom( + info["nom"], uppercase=False + ) # strcapitalize(strlower(info['nom'])) + else: + n = user_name + + prenom_abbrv = scu.abbrev_prenom(p) + # nomprenom est le nom capitalisé suivi de l'initiale du prénom + info["nomprenom"] = (n + " " + prenom_abbrv).strip() + # prenomnom est l'initiale du prénom suivie du nom + info["prenomnom"] = (prenom_abbrv + " " + n).strip() + # nom_fmt et prenom_fmt: minuscule capitalisé + info["nom_fmt"] = n + info["prenom_fmt"] = sco_etud.format_prenom(p) + # nomcomplet est le prenom et le nom complets + info["nomcomplet"] = info["prenom_fmt"] + " " + info["nom_fmt"] + # nomplogin est le nom en majuscules suivi du prénom et du login + # e.g. Dupont Pierre (dupont) + info["nomplogin"] = "%s %s (%s)" % (scu.strupper(n), p, info["user_name"]) + # nomnoacc est le nom en minuscules sans accents + info["nomnoacc"] = scu.suppress_accents(scu.strlower(info["nom"])) + + return info + + +def user_info_page(context, user_name=None, REQUEST=None): + """Display page of info about given user. + If user_name not specified, user current_user + """ + # peut on divulguer ces infos ? + if not can_handle_passwd(current_user, allow_admindepts=True): + raise AccessDenied("Vous n'avez pas la permission de voir cette page") + + dept = g.scodoc_dept + if not user_name: + user = current_user + else: + user = User.query.filter_by(user_name=user_name).first() + if not user: + raise ScoValueError("invalid user_name") + H = [ + html_sco_header.sco_header( + context, + REQUEST, + page_title="Utilisateur %s" % user.user_name.encode("utf-8"), # sco8 + ) + ] + F = html_sco_header.sco_footer(context, REQUEST) + H.append("

Utilisateur: %s" % user.user_name) + info = user.to_dict() + if info: + H.append(" (%(status_txt)s)" % info) + H.append("

") + if not info: + H.append( + "

L' utilisateur '%s' n'est pas défini dans ce module.

" % user_name + ) + if user.has_permission(Permission.ScoEditAllNotes, dept): + H.append("

(il peut modifier toutes les notes de %s)

" % dept) + if user.has_permission(Permission.ScoEditAllEvals, dept): + H.append("

(il peut modifier toutes les évaluations de %s)

" % dept) + if user.has_permission(Permission.ScoImplement, dept): + H.append("

(il peut creer des formations en %s)

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

+ Login : %(user_name)s
+ Nom : %(nom)s
+ Prénom : %(prenom)s
+ Mail : %(email)s
+ Roles : %(roles_string)s
+ Dept : %(dept)s
+ Dernière modif mot de passe: %(date_modif_passwd)s
+ Date d'expiration: %(date_expiration)s +

") + + if current_user.user_name == user_name: + H.append( + '

Se déconnecter: logout

' + % url_for("auth.logout") + ) + # Liste des permissions + H.append( + '

Permissions de cet utilisateur dans le département %s:

") + + if current_user.has_permission(Permission.ScoUsersAdmin, dept): + H.append( + '

Liste de tous les utilisateurs

' + % url_for("users.index_html", scodoc_dept=g.scodoc_dept) + ) + return scu.sco8_join(H) + F diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 0c9b6e4491..ee025b07e5 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -68,6 +68,12 @@ from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL from app.scodoc import sco_exceptions from app.scodoc import VERSION +# ----- TEMPORAIRE POUR MIGRATION SCODOC7 -> SCODOC8 avant python3 +def sco8_join(L, sep="\n"): # sco8 + return sep.join( + [x if not isinstance(x, types.UnicodeType) else x.encode("utf-8") for x in L] + ) + # ----- CALCUL ET PRESENTATION DES NOTES NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis diff --git a/app/views/users.py b/app/views/users.py index 456f62bd58..24a2899b2e 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -35,7 +35,7 @@ Emmanuel Viennet, 2021 """ from flask import g -from flask import current_app +from flask import current_app, request from app.auth.models import Permission from app.auth.models import User @@ -45,9 +45,11 @@ from app.decorators import ( permission_required, admin_required, login_required, + ZRequest, ) from app.scodoc import sco_users +from app.scodoc import sco_utils as scu from app.views import users_bp as bp @@ -55,21 +57,28 @@ from app.views import users_bp as bp context = ScoDoc7Context("users") # sco8 -# ------- Fonctions vraiment spécifiques à ScoDoc: @bp.route("/") @bp.route("/index_html") @permission_required(Permission.ScoUsersView) @scodoc7func(context) -def index_html(context, REQUEST, all_depts=False, with_olds=False, format="html"): +def index_html(context, REQUEST, all_depts=False, with_inactives=False, format="html"): return sco_users.index_html( context, REQUEST=REQUEST, all_depts=all_depts, - with_olds=with_olds, + with_inactives=with_inactives, format=format, ) +@bp.route("/user_info") +@permission_required(Permission.ScoUsersView) +@scodoc7func(context) +def user_info(user_name, format="json", REQUEST=None): + info = sco_users.user_info(user_name=user_name) + return scu.sendResult(REQUEST, info, name="user", format=format) + + @bp.route("/create_user_form") def create_user_form(): raise NotImplementedError() @@ -80,6 +89,7 @@ def import_users_form(): raise NotImplementedError() -@bp.route("/userinfo") -def userinfo(): - raise NotImplementedError() +@bp.route("/user_info_page") +@scodoc7func(context) +def user_info_page(user_name, REQUEST=None): + return sco_users.user_info_page(context, user_name=user_name, REQUEST=REQUEST) diff --git a/scodoc_manager.py b/scodoc_manager.py index d3ed4d839c..5cbadf34a2 100644 --- a/scodoc_manager.py +++ b/scodoc_manager.py @@ -98,5 +98,5 @@ class FakeUsers(object): "date_expiration": None, } - def get_user_list(self, dept=None, with_olds=False): + def get_user_list(self, dept=None, with_inactives=False): return [self.user_info()]