forked from ScoDoc/ScoDoc
API: enrichit création/édition User
This commit is contained in:
parent
939371cff9
commit
4d3cbf7e75
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 += [
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user