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
"""
import datetime
from flask import g, request
from flask_json import as_json
@ -85,6 +85,20 @@ def users_info_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"])
@api_web_bp.route("/user/create", methods=["POST"])
@login_required
@ -95,21 +109,22 @@ def user_create():
"""Création d'un utilisateur
The request content type should be "application/json":
{
"user_name": str,
"active":bool (default True),
"dept": str or null,
"nom": str,
"prenom": str,
"active":bool (default True)
"user_name": str,
...
}
"""
data = request.get_json(force=True) # may raise 400 Bad Request
user_name = data.get("user_name")
args = request.get_json(force=True) # may raise 400 Bad Request
user_name = args.get("user_name")
if not user_name:
return json_error(404, "empty user_name")
user = User.query.filter_by(user_name=user_name).first()
if user:
return json_error(404, f"user_create: user {user} already exists\n")
dept = data.get("dept")
dept = args.get("dept")
if dept == "@all":
dept = None
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
):
return json_error(404, "user_create: departement inexistant")
nom = data.get("nom")
prenom = data.get("prenom")
active = scu.to_bool(data.get("active", True))
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
args["dept"] = dept
ok, msg = _is_allowed_user_edit(args)
if not ok:
return json_error(403, f"user_create: {msg}")
user = User()
user.from_dict(args, new_user=True)
db.session.add(user)
db.session.commit()
return user.to_dict()
@ -142,13 +159,14 @@ def user_edit(uid: int):
"nom": str,
"prenom": str,
"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)
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
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 == "@all":
dest_dept = None
@ -164,10 +182,11 @@ def user_edit(uid: int):
return json_error(404, "user_edit: departement inexistant")
user.dept = dest_dept
user.nom = data.get("nom", user.nom)
user.prenom = data.get("prenom", user.prenom)
user.active = scu.to_bool(data.get("active", user.active))
ok, msg = _is_allowed_user_edit(args)
if not ok:
return json_error(403, f"user_edit: {msg}")
user.from_dict(args)
db.session.add(user)
db.session.commit()
return user.to_dict()

View File

@ -21,7 +21,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
import jwt
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.config import ScoDocSiteConfig
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"""
id = db.Column(db.Integer, primary_key=True)
@ -121,7 +121,7 @@ class User(UserMixin, db.Model):
# check login:
if kwargs.get("user_name") and 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:
if (
not self.roles
@ -251,12 +251,13 @@ class User(UserMixin, db.Model):
"cas_last_login": self.cas_last_login.isoformat() + "Z"
if self.cas_last_login
else None,
"edt_id": self.edt_id,
"status_txt": "actif" if self.active else "fermé",
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
"nom": (self.nom or ""), # sco8
"prenom": (self.prenom or ""), # sco8
"nom": self.nom or "",
"prenom": self.prenom or "",
"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:
"nom_fmt": self.get_nom_fmt(),
"prenom_fmt": self.get_prenom_fmt(),
@ -270,28 +271,34 @@ class User(UserMixin, db.Model):
data["email_institutionnel"] = self.email_institutionnel or ""
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):
"""Set users' attributes from given dict values.
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
Does not check permissions here.
"""
for field in [
"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)))
super().from_dict(data, excluded=("user_name", "roles_string"))
if new_user:
if "user_name" in data:

View File

@ -78,9 +78,11 @@ class ScoDocModel:
# virtual, by default, do nothing
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."
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():
if hasattr(self, key):
setattr(self, key, value)

View File

@ -102,7 +102,7 @@ def index_html(
<option value="">--Choisir--</option>
{menu_roles}
</select>
</form>
"""
)
@ -204,7 +204,7 @@ def list_users(
"cas_allow_scodoc_login",
"cas_last_login",
]
columns_ids.append("email_institutionnel")
columns_ids += ["email_institutionnel", "edt_id"]
title = "Utilisateurs définis dans ScoDoc"
tab = GenTable(
@ -227,6 +227,7 @@ def list_users(
"cas_allow_login": "CAS autorisé",
"cas_allow_scodoc_login": "Cnx sans CAS",
"cas_last_login": "Dernier login CAS",
"edt_id": "Identifiant emploi du temps",
},
caption=title,
page_title="title",

View File

@ -4347,6 +4347,10 @@ table.dataTable td.group {
background: #fff;
}
#zonePartitions .edt_id {
color: rgb(85, 255, 24);
}
/* ------------- Nouveau tableau recap ------------ */
div.table_recap {
margin-top: 6px;
@ -4856,7 +4860,3 @@ div.cas_etat_certif_ssl {
font-style: italic;
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>Mail :</b> {{user.email}}<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>Dept :</b> {{user.dept or ""}}<br>
{% 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,
},
),
(
"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
descr += [

View File

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

View File

@ -123,3 +123,31 @@ def test_create_delete(test_client):
db.session.commit()
ul = User.query.filter_by(prenom="Pierre").all()
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()