API: enrichit création/édition User

This commit is contained in:
Emmanuel Viennet 2023-11-21 22:28:50 +01:00
parent 939371cff9
commit 4d3cbf7e75
9 changed files with 119 additions and 48 deletions

View File

@ -7,7 +7,7 @@
""" """
ScoDoc 9 API : accès aux utilisateurs ScoDoc 9 API : accès aux utilisateurs
""" """
import datetime
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
@ -85,6 +85,20 @@ def users_info_query():
return [user.to_dict() for user in query] return [user.to_dict() for user in query]
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
"Vrai si on peut"
if "cas_id" in args and not current_user.has_permission(
Permission.UsersChangeCASId
):
return False, "non autorise a changer cas_id"
if not current_user.is_administrator():
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
if field in args:
return False, f"non autorise a changer {field}"
return True, ""
@bp.route("/user/create", methods=["POST"]) @bp.route("/user/create", methods=["POST"])
@api_web_bp.route("/user/create", methods=["POST"]) @api_web_bp.route("/user/create", methods=["POST"])
@login_required @login_required
@ -95,21 +109,22 @@ def user_create():
"""Création d'un utilisateur """Création d'un utilisateur
The request content type should be "application/json": The request content type should be "application/json":
{ {
"user_name": str, "active":bool (default True),
"dept": str or null, "dept": str or null,
"nom": str, "nom": str,
"prenom": str, "prenom": str,
"active":bool (default True) "user_name": str,
...
} }
""" """
data = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
user_name = data.get("user_name") user_name = args.get("user_name")
if not user_name: if not user_name:
return json_error(404, "empty user_name") return json_error(404, "empty user_name")
user = User.query.filter_by(user_name=user_name).first() user = User.query.filter_by(user_name=user_name).first()
if user: if user:
return json_error(404, f"user_create: user {user} already exists\n") return json_error(404, f"user_create: user {user} already exists\n")
dept = data.get("dept") dept = args.get("dept")
if dept == "@all": if dept == "@all":
dept = None dept = None
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin) allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
@ -119,10 +134,12 @@ def user_create():
Departement.query.filter_by(acronym=dept).first() is None Departement.query.filter_by(acronym=dept).first() is None
): ):
return json_error(404, "user_create: departement inexistant") return json_error(404, "user_create: departement inexistant")
nom = data.get("nom") args["dept"] = dept
prenom = data.get("prenom") ok, msg = _is_allowed_user_edit(args)
active = scu.to_bool(data.get("active", True)) if not ok:
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom) return json_error(403, f"user_create: {msg}")
user = User()
user.from_dict(args, new_user=True)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user.to_dict() return user.to_dict()
@ -142,13 +159,14 @@ def user_edit(uid: int):
"nom": str, "nom": str,
"prenom": str, "prenom": str,
"active":bool "active":bool
...
} }
""" """
data = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
user: User = User.query.get_or_404(uid) user: User = User.query.get_or_404(uid)
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée # L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
orig_dept = user.dept orig_dept = user.dept
dest_dept = data.get("dept", False) dest_dept = args.get("dept", False)
if dest_dept is not False: if dest_dept is not False:
if dest_dept == "@all": if dest_dept == "@all":
dest_dept = None dest_dept = None
@ -164,10 +182,11 @@ def user_edit(uid: int):
return json_error(404, "user_edit: departement inexistant") return json_error(404, "user_edit: departement inexistant")
user.dept = dest_dept user.dept = dest_dept
user.nom = data.get("nom", user.nom) ok, msg = _is_allowed_user_edit(args)
user.prenom = data.get("prenom", user.prenom) if not ok:
user.active = scu.to_bool(data.get("active", user.active)) return json_error(403, f"user_edit: {msg}")
user.from_dict(args)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user.to_dict() return user.to_dict()

View File

@ -21,7 +21,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
import jwt import jwt
from app import db, email, log, login from app import db, email, log, login
from app.models import Departement from app.models import Departement, ScoDocModel
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -59,7 +59,7 @@ def invalid_user_name(user_name: str) -> bool:
) )
class User(UserMixin, db.Model): class User(UserMixin, db.Model, ScoDocModel):
"""ScoDoc users, handled by Flask / SQLAlchemy""" """ScoDoc users, handled by Flask / SQLAlchemy"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -121,7 +121,7 @@ class User(UserMixin, db.Model):
# check login: # check login:
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]): if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]):
raise ValueError(f"invalid user_name: {kwargs['user_name']}") raise ValueError(f"invalid user_name: {kwargs['user_name']}")
super(User, self).__init__(**kwargs) super().__init__(**kwargs)
# Ajoute roles: # Ajoute roles:
if ( if (
not self.roles not self.roles
@ -251,12 +251,13 @@ class User(UserMixin, db.Model):
"cas_last_login": self.cas_last_login.isoformat() + "Z" "cas_last_login": self.cas_last_login.isoformat() + "Z"
if self.cas_last_login if self.cas_last_login
else None, else None,
"edt_id": self.edt_id,
"status_txt": "actif" if self.active else "fermé", "status_txt": "actif" if self.active else "fermé",
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None, "last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
"nom": (self.nom or ""), # sco8 "nom": self.nom or "",
"prenom": (self.prenom or ""), # sco8 "prenom": self.prenom or "",
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info" "roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
"user_name": self.user_name, # sco8 "user_name": self.user_name,
# Les champs calculés: # Les champs calculés:
"nom_fmt": self.get_nom_fmt(), "nom_fmt": self.get_nom_fmt(),
"prenom_fmt": self.get_prenom_fmt(), "prenom_fmt": self.get_prenom_fmt(),
@ -270,28 +271,34 @@ class User(UserMixin, db.Model):
data["email_institutionnel"] = self.email_institutionnel or "" data["email_institutionnel"] = self.email_institutionnel or ""
return data return data
@classmethod
def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict. No other side effect.
args: dict with args in application.
returns: dict to store in model's db.
Convert boolean values to bools.
"""
args_dict = args
if "date_expiration" in args:
date_expiration = args.get("date_expiration")
if isinstance(date_expiration, str):
args["date_expiration"] = (
datetime.datetime.fromisoformat(date_expiration)
if date_expiration
else None
)
for field in ("active", "cas_allow_login", "cas_allow_scodoc_login"):
if field in args:
args_dict[field] = scu.to_bool(args.get(field))
return args_dict
def from_dict(self, data: dict, new_user=False): def from_dict(self, data: dict, new_user=False):
"""Set users' attributes from given dict values. """Set users' attributes from given dict values.
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ" Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
Does not check permissions here.
""" """
for field in [ super().from_dict(data, excluded=("user_name", "roles_string"))
"nom",
"prenom",
"dept",
"active",
"email",
"email_institutionnel",
"date_expiration",
"cas_id",
]:
if field in data:
setattr(self, field, data[field] or None)
# required boolean fields
for field in [
"cas_allow_login",
"cas_allow_scodoc_login",
]:
setattr(self, field, scu.to_bool(data.get(field, False)))
if new_user: if new_user:
if "user_name" in data: if "user_name" in data:

View File

@ -78,9 +78,11 @@ class ScoDocModel:
# virtual, by default, do nothing # virtual, by default, do nothing
return args return args
def from_dict(self, args: dict): def from_dict(self, args: dict, excluded: set[str] = None):
"Update object's fields given in dict. Add to session but don't commit." "Update object's fields given in dict. Add to session but don't commit."
args_dict = self.convert_dict_fields(self.filter_model_attributes(args)) args_dict = self.convert_dict_fields(
self.filter_model_attributes(args, excluded=excluded)
)
for key, value in args_dict.items(): for key, value in args_dict.items():
if hasattr(self, key): if hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)

View File

@ -204,7 +204,7 @@ def list_users(
"cas_allow_scodoc_login", "cas_allow_scodoc_login",
"cas_last_login", "cas_last_login",
] ]
columns_ids.append("email_institutionnel") columns_ids += ["email_institutionnel", "edt_id"]
title = "Utilisateurs définis dans ScoDoc" title = "Utilisateurs définis dans ScoDoc"
tab = GenTable( tab = GenTable(
@ -227,6 +227,7 @@ def list_users(
"cas_allow_login": "CAS autorisé", "cas_allow_login": "CAS autorisé",
"cas_allow_scodoc_login": "Cnx sans CAS", "cas_allow_scodoc_login": "Cnx sans CAS",
"cas_last_login": "Dernier login CAS", "cas_last_login": "Dernier login CAS",
"edt_id": "Identifiant emploi du temps",
}, },
caption=title, caption=title,
page_title="title", page_title="title",

View File

@ -4347,6 +4347,10 @@ table.dataTable td.group {
background: #fff; background: #fff;
} }
#zonePartitions .edt_id {
color: rgb(85, 255, 24);
}
/* ------------- Nouveau tableau recap ------------ */ /* ------------- Nouveau tableau recap ------------ */
div.table_recap { div.table_recap {
margin-top: 6px; margin-top: 6px;
@ -4856,7 +4860,3 @@ div.cas_etat_certif_ssl {
font-style: italic; font-style: italic;
color: rgb(231, 0, 0); color: rgb(231, 0, 0);
} }
.edt_id {
color: rgb(85, 255, 24);
}

View File

@ -18,6 +18,7 @@
<b>Prénom :</b> {{user.prenom or ""}}<br> <b>Prénom :</b> {{user.prenom or ""}}<br>
<b>Mail :</b> {{user.email}}<br> <b>Mail :</b> {{user.email}}<br>
<b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br> <b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br>
<b>Identifiant EDT:</b> {{user.edt_id or ""}}<br>
<b>Rôles :</b> {{user.get_roles_string()}}<br> <b>Rôles :</b> {{user.get_roles_string()}}<br>
<b>Dept :</b> {{user.dept or ""}}<br> <b>Dept :</b> {{user.dept or ""}}<br>
{% if user.passwd_temp or user.password_scodoc7 %} {% if user.passwd_temp or user.password_scodoc7 %}

View File

@ -450,6 +450,17 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"readonly": edit_only_roles, "readonly": edit_only_roles,
}, },
), ),
(
"edt_id",
{
"title": "Identifiant sur l'emploi du temps",
"input_type": "text",
"explanation": """id du compte utilisateur sur l'emploi du temps
ou l'annuaire de l'établissement (par défaut, l'e-mail institutionnel )""",
"size": 36,
"allow_null": True,
},
),
] ]
if not edit: # options création utilisateur if not edit: # options création utilisateur
descr += [ descr += [

View File

@ -88,15 +88,17 @@ def test_edit_users(api_admin_headers):
# Change le dept et rend inactif # Change le dept et rend inactif
user = POST_JSON( user = POST_JSON(
f"/user/{user['id']}/edit", f"/user/{user['id']}/edit",
{"active": False, "dept": "TAPI"}, {"active": False, "dept": "TAPI", "edt_id": "GGG"},
headers=admin_h, headers=admin_h,
) )
assert user["dept"] == "TAPI" assert user["dept"] == "TAPI"
assert user["active"] is False assert user["active"] is False
assert user["edt_id"] == "GGG"
user = GET(f"/user/{user['id']}", headers=admin_h) user = GET(f"/user/{user['id']}", headers=admin_h)
assert user["nom"] == "Toto" assert user["nom"] == "Toto"
assert user["dept"] == "TAPI" assert user["dept"] == "TAPI"
assert user["active"] is False assert user["active"] is False
assert user["edt_id"] == "GGG"
def test_roles(api_admin_headers): def test_roles(api_admin_headers):

View File

@ -123,3 +123,31 @@ def test_create_delete(test_client):
db.session.commit() db.session.commit()
ul = User.query.filter_by(prenom="Pierre").all() ul = User.query.filter_by(prenom="Pierre").all()
assert len(ul) == 1 assert len(ul) == 1
def test_edit(test_client):
"test edition object utlisateur"
args = {
"user_name": "Tonari",
"prenom": "No Totoro",
"edt_id": "totorito",
"cas_allow_login": 1, # boolean
"irrelevant": "..", # intentionnellement en dehors des attributs
}
u = User()
u.from_dict(args)
db.session.add(u)
db.session.commit()
db.session.refresh(u)
assert u.edt_id == "totorito"
assert u.nom is None
assert u.cas_allow_login is True
d = u.to_dict()
assert d["nom"] == ""
args["cas_allow_login"] = 0
u.from_dict(args)
db.session.commit()
db.session.refresh(u)
assert u.cas_allow_login is False
db.session.delete(u)
db.session.commit()