Page info utilisateur

This commit is contained in:
Emmanuel Viennet 2021-06-27 12:11:39 +02:00
parent f991ffdca5
commit d40a2b43cd
10 changed files with 227 additions and 1362 deletions

View File

@ -32,7 +32,7 @@ class User(UserMixin, db.Model):
nom = db.Column(db.String(64)) nom = db.Column(db.String(64))
prenom = db.Column(db.String(64)) prenom = db.Column(db.String(64))
dept = db.Column(db.String(32), index=True) 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)) password_hash = db.Column(db.String(128))
last_seen = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow)
@ -48,6 +48,7 @@ class User(UserMixin, db.Model):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.roles = [] self.roles = []
self.user_roles = []
super(User, self).__init__(**kwargs) super(User, self).__init__(**kwargs)
if ( if (
not self.roles not self.roles
@ -78,6 +79,8 @@ class User(UserMixin, db.Model):
"""Check given password vs current one. """Check given password vs current one.
Returns `True` if the password matched, `False` otherwise. 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 if not self.password_hash: # user without password can't login
return False return False
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
@ -99,30 +102,29 @@ class User(UserMixin, db.Model):
return return
return User.query.get(id) return User.query.get(id)
def to_dict(self, include_email=False): def to_dict(self, include_email=True):
data = { data = {
"date_expiration": self.date_expiration.isoformat() + "Z" "date_expiration": self.date_expiration.isoformat() + "Z"
if self.date_expiration if self.date_expiration
else None, else "",
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z" "date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
if self.date_modif_passwd if self.date_modif_passwd
else None, else "",
"date_created": self.date_created.isoformat() + "Z" "date_created": self.date_created.isoformat() + "Z"
if self.date_created if self.date_created
else None, else "",
"dept": self.dept, "dept": (self.dept or "").encode("utf-8"), # sco8
"id": self.id, "id": self.id,
"is_old": self.is_old, "active": self.active,
"status_txt": "(ancien)" if self.is_old else "", "status_txt": "actif" if self.active else "fermé",
"last_seen": self.last_seen.isoformat() + "Z", "last_seen": self.last_seen.isoformat() + "Z",
"nom": self.nom, "nom": (self.nom or "").encode("utf-8"), # sco8
"prenom": self.prenom, "prenom": (self.prenom or "").encode("utf-8"), # sco8
"roles": self.roles,
"roles_string": self.get_roles_string(), "roles_string": self.get_roles_string(),
"user_name": self.user_name, "user_name": self.user_name.encode("utf-8"), # sco8
} }
if include_email: if include_email:
data["email"] = self.email data["email"] = self.email or ""
return data return data
def from_dict(self, data, new_user=False): def from_dict(self, data, new_user=False):
@ -154,12 +156,14 @@ class User(UserMixin, db.Model):
# Permissions management: # Permissions management:
def has_permission(self, perm, dept=False): def has_permission(self, perm, dept=False):
"""Check if user has permission `perm` in given `dept`. """Check if user has permission `perm` in given `dept`.
Emulate Zope `has_permission`` Similar to Zope ScoDoc7 `has_permission``
Args: Args:
perm: integer, one of the value defined in Permission class. perm: integer, one of the value defined in Permission class.
dept: dept id (eg 'RT') dept: dept id (eg 'RT')
""" """
if not self.active:
return False
if dept is False: if dept is False:
dept = g.scodoc_dept dept = g.scodoc_dept
# les role liés à ce département, et les roles avec dept=None (super-admin) # 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) return ", ".join("{r.role.name}{r.dept}".format(r=r) for r in self.user_roles)
def is_administrator(self): def is_administrator(self):
return self.has_permission(Permission.ScoSuperAdmin, None) return self.active and self.has_permission(Permission.ScoSuperAdmin, None)
class AnonymousUser(AnonymousUserMixin): class AnonymousUser(AnonymousUserMixin):

View File

@ -142,7 +142,7 @@ def scodoc7func(context):
return func(*args, **kwargs) return func(*args, **kwargs)
# #
if "scodoc_dept" in 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"] del kwargs["scodoc_dept"]
elif not hasattr(g, "scodoc_dept"): elif not hasattr(g, "scodoc_dept"):
g.scodoc_dept = None g.scodoc_dept = None

File diff suppressed because it is too large Load Diff

View File

@ -334,7 +334,7 @@ def sco_header(
# div pour affichage messages temporaires # div pour affichage messages temporaires
H.append('<div id="sco_msg" class="head_message"></div>') H.append('<div id="sco_msg" class="head_message"></div>')
# #
return "".join(H) return scu.sco8_join(H, sep="")
def sco_footer(context, REQUEST=None): def sco_footer(context, REQUEST=None):

View File

@ -42,11 +42,12 @@ def sidebar_common(context, REQUEST=None):
"UsersURL": scu.UsersURL(), "UsersURL": scu.UsersURL(),
"NotesURL": scu.NotesURL(), "NotesURL": scu.NotesURL(),
"AbsencesURL": scu.AbsencesURL(), "AbsencesURL": scu.AbsencesURL(),
# XXX TODO "LogoutURL" : ,
"authuser": str(authuser), "authuser": str(authuser),
} }
H = [ H = [
'<a class="scodoc_title" href="about">ScoDoc</a>', '<a class="scodoc_title" href="about">ScoDoc</a>',
'<div id="authuser"><a id="authuserlink" href="%(ScoURL)s/Users/userinfo">%(authuser)s</a><br/><a id="deconnectlink" href="%(ScoURL)s/acl_users/logout">déconnexion</a></div>' '<div id="authuser"><a id="authuserlink" href="%(ScoURL)s/Users/user_info_page">%(authuser)s</a><br/><a id="deconnectlink" href="%(ScoURL)s/acl_users/logout">déconnexion</a></div>'
% params, % params,
sidebar_dept(context, REQUEST), sidebar_dept(context, REQUEST),
"""<h2 class="insidebar">Scolarit&eacute;</h2> """<h2 class="insidebar">Scolarit&eacute;</h2>

View File

@ -50,8 +50,6 @@ class Permission:
def init_permissions(): def init_permissions():
for (perm, symbol, description) in _SCO_PERMISSIONS: for (perm, symbol, description) in _SCO_PERMISSIONS:
setattr(Permission, symbol, perm) setattr(Permission, symbol, perm)
# Crée aussi les attributs dans le module (ScoDoc7 compat)
globals()[symbol] = perm
Permission.description[symbol] = description Permission.description[symbol] = description
Permission.NBITS = len(_SCO_PERMISSIONS) Permission.NBITS = len(_SCO_PERMISSIONS)

View File

@ -30,12 +30,12 @@
# Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy # 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 base64
import jaxml import jaxml
import md5
import re
import string
import time
from flask import current_app, url_for, g from flask import current_app, url_for, g
from flask_login import current_user from flask_login import current_user
@ -94,10 +94,10 @@ def is_valid_password(cleartxt):
# XXX TODO supprimer ndb.GetUsersDBConnexion # 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..." "gestion utilisateurs..."
all_depts = int(all_depts) 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 = [html_sco_header.html_sem_header(context, REQUEST, "Gestion des utilisateurs")]
H.append("<h2>Gestion des utilisateurs</h2>") H.append("<h2>Gestion des utilisateurs</h2>")
@ -121,14 +121,14 @@ def index_html(context, REQUEST, all_depts=False, with_olds=False, format="html"
checked = "checked" checked = "checked"
else: else:
checked = "" checked = ""
if with_olds: if with_inactives:
olds_checked = "checked" olds_checked = "checked"
else: else:
olds_checked = "" olds_checked = ""
H.append( H.append(
"""<p><form name="f" action="%s" method="get"> """<p><form name="f" action="%s" method="get">
<input type="checkbox" name="all_depts" value="1" onchange="document.f.submit();" %s>Tous les départements</input> <input type="checkbox" name="all_depts" value="1" onchange="document.f.submit();" %s>Tous les départements</input>
<input type="checkbox" name="with_olds" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input> <input type="checkbox" name="with_inactives" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input>
</form></p>""" </form></p>"""
% (REQUEST.URL0, checked, olds_checked) % (REQUEST.URL0, checked, olds_checked)
) )
@ -137,7 +137,7 @@ def index_html(context, REQUEST, all_depts=False, with_olds=False, format="html"
context, context,
g.scodoc_dept, g.scodoc_dept,
all_depts=all_depts, all_depts=all_depts,
with_olds=with_olds, with_inactives=with_inactives,
format=format, format=format,
REQUEST=REQUEST, REQUEST=REQUEST,
with_links=current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept), with_links=current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept),
@ -154,19 +154,19 @@ def list_users(
context, context,
dept, dept,
all_depts=False, # tous les departements 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", format="html",
with_links=True, with_links=True,
REQUEST=None, REQUEST=None,
): ):
"List users, returns a table in the specified format" "List users, returns a table in the specified format"
if dept and not all_depts: 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 comm = "dept. %s" % dept.encode(scu.SCO_ENCODING) # sco8
else: else:
r = get_user_list(with_olds=with_olds) r = get_user_list(with_inactives=with_inactives)
comm = "tous" comm = "tous"
if with_olds: if with_inactives:
comm += ", avec anciens" comm += ", avec anciens"
comm = "(" + comm + ")" comm = "(" + comm + ")"
# -- Add some information and links: # -- Add some information and links:
@ -180,7 +180,7 @@ def list_users(
# Add links # Add links
if with_links and can_modify: if with_links and can_modify:
target = url_for( 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( ).encode(
scu.SCO_ENCODING scu.SCO_ENCODING
) # sco8 ) # 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. """Returns list of users.
If dept, select users from this dept, If dept, select users from this dept,
else return all users. else return all users.
@ -246,10 +246,170 @@ def get_user_list(dept=None, with_olds=False):
q = User.query q = User.query
if dept is not None: if dept is not None:
q = q.filter_by(dept=dept) q = q.filter_by(dept=dept)
if not with_olds: if not with_inactives:
q = q.filter_by(is_old=False) q = q.filter_by(active=True)
return q.all() return q.order_by(User.nom, User.user_name).all()
# def get_user_infos(user_list): def _user_list(user_name):
# return [ user_info(u) for u in user_list ] "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("<h2>Utilisateur: %s" % user.user_name)
info = user.to_dict()
if info:
H.append(" (%(status_txt)s)" % info)
H.append("</h2>")
if not info:
H.append(
"<p>L' utilisateur '%s' n'est pas défini dans ce module.</p>" % user_name
)
if user.has_permission(Permission.ScoEditAllNotes, dept):
H.append("<p>(il peut modifier toutes les notes de %s)</p>" % dept)
if user.has_permission(Permission.ScoEditAllEvals, dept):
H.append("<p>(il peut modifier toutes les évaluations de %s)</p>" % dept)
if user.has_permission(Permission.ScoImplement, dept):
H.append("<p>(il peut creer des formations en %s)</p>" % dept)
else:
H.append(
"""<p>
<b>Login :</b> %(user_name)s<br/>
<b>Nom :</b> %(nom)s<br/>
<b>Prénom :</b> %(prenom)s<br/>
<b>Mail :</b> %(email)s<br/>
<b>Roles :</b> %(roles_string)s<br/>
<b>Dept :</b> %(dept)s<br/>
<b>Dernière modif mot de passe:</b> %(date_modif_passwd)s<br/>
<b>Date d'expiration:</b> %(date_expiration)s
<p><ul>
<li><a class="stdlink" href="form_change_password?user_name=%(user_name)s">changer le mot de passe</a></li>"""
% info
)
if current_user.has_permission(Permission.ScoUsersAdmin, dept):
H.append(
"""
<li><a class="stdlink" href="create_user_form?user_name=%(user_name)s&edit=1">modifier/déactiver ce compte</a></li>
<li><a class="stdlink" href="delete_user_form?user_name=%(user_name)s">supprimer cet utilisateur</a> <em>(à n'utiliser qu'en cas d'erreur !)</em></li>
"""
% info
)
H.append("</ul>")
if current_user.user_name == user_name:
H.append(
'<p><b>Se déconnecter: <a class="stdlink" href="%s">logout</a></b></p>'
% url_for("auth.logout")
)
# Liste des permissions
H.append(
'<div class="permissions"><p>Permissions de cet utilisateur dans le département %s:</p><ul>'
% dept
)
for p in Permission.description:
perm = getattr(Permission, p)
if user.has_permission(perm, dept):
b = "oui"
else:
b = "non"
H.append("<li>%s : %s</li>" % (Permission.description[p], b))
H.append("</ul></div>")
if current_user.has_permission(Permission.ScoUsersAdmin, dept):
H.append(
'<p><a class="stdlink" href="%s">Liste de tous les utilisateurs</a></p>'
% url_for("users.index_html", scodoc_dept=g.scodoc_dept)
)
return scu.sco8_join(H) + F

View File

@ -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 sco_exceptions
from app.scodoc import VERSION 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 # ----- CALCUL ET PRESENTATION DES NOTES
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis

View File

@ -35,7 +35,7 @@ Emmanuel Viennet, 2021
""" """
from flask import g 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 Permission
from app.auth.models import User from app.auth.models import User
@ -45,9 +45,11 @@ from app.decorators import (
permission_required, permission_required,
admin_required, admin_required,
login_required, login_required,
ZRequest,
) )
from app.scodoc import sco_users from app.scodoc import sco_users
from app.scodoc import sco_utils as scu
from app.views import users_bp as bp from app.views import users_bp as bp
@ -55,21 +57,28 @@ from app.views import users_bp as bp
context = ScoDoc7Context("users") # sco8 context = ScoDoc7Context("users") # sco8
# ------- Fonctions vraiment spécifiques à ScoDoc:
@bp.route("/") @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html")
@permission_required(Permission.ScoUsersView) @permission_required(Permission.ScoUsersView)
@scodoc7func(context) @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( return sco_users.index_html(
context, context,
REQUEST=REQUEST, REQUEST=REQUEST,
all_depts=all_depts, all_depts=all_depts,
with_olds=with_olds, with_inactives=with_inactives,
format=format, 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") @bp.route("/create_user_form")
def create_user_form(): def create_user_form():
raise NotImplementedError() raise NotImplementedError()
@ -80,6 +89,7 @@ def import_users_form():
raise NotImplementedError() raise NotImplementedError()
@bp.route("/userinfo") @bp.route("/user_info_page")
def userinfo(): @scodoc7func(context)
raise NotImplementedError() def user_info_page(user_name, REQUEST=None):
return sco_users.user_info_page(context, user_name=user_name, REQUEST=REQUEST)

View File

@ -98,5 +98,5 @@ class FakeUsers(object):
"date_expiration": None, "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()] return [self.user_info()]