API: users, WIP
This commit is contained in:
parent
dae762c3b1
commit
a069280746
171
app/api/users.py
171
app/api/users.py
@ -21,6 +21,7 @@ from app.decorators import scodoc, permission_required
|
|||||||
from app.models import Departement
|
from app.models import Departement
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/user/<int:uid>")
|
@bp.route("/user/<int:uid>")
|
||||||
@ -78,4 +79,172 @@ def users_info_query():
|
|||||||
)
|
)
|
||||||
|
|
||||||
query = query.order_by(User.user_name)
|
query = query.order_by(User.user_name)
|
||||||
return jsonify([u.to_dict() for u in query])
|
return jsonify([user.to_dict() for user in query])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/user/create", methods=["POST"])
|
||||||
|
@api_web_bp.route("/user/create", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoUsersAdmin)
|
||||||
|
def user_create():
|
||||||
|
"""Création d'un utilisateur
|
||||||
|
The request content type should be "application/json":
|
||||||
|
{
|
||||||
|
"user_name": str,
|
||||||
|
"dept": str or null,
|
||||||
|
"nom": str,
|
||||||
|
"prenom": str,
|
||||||
|
"active":bool (default True)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
|
user_name = data.get("user_name")
|
||||||
|
if not user_name:
|
||||||
|
return error_response(404, "empty user_name")
|
||||||
|
user = User.query.filter_by(user_name=user_name).first()
|
||||||
|
if user:
|
||||||
|
return error_response(404, f"user_create: user {user} already exists\n")
|
||||||
|
dept = data.get("dept")
|
||||||
|
if dept == "@all":
|
||||||
|
dept = None
|
||||||
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
||||||
|
if dept not in allowed_depts:
|
||||||
|
return error_response(403, "user_create: departement non autorise")
|
||||||
|
if (dept is not None) and (
|
||||||
|
Departement.query.filter_by(acronym=dept).first() is None
|
||||||
|
):
|
||||||
|
return error_response(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)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(user.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/user/edit/<int:uid>", methods=["POST"])
|
||||||
|
@api_web_bp.route("/user/edit/<int:uid>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoUsersAdmin)
|
||||||
|
def user_edit(uid: int):
|
||||||
|
"""Modification d'un utilisateur
|
||||||
|
Champs modifiables:
|
||||||
|
{
|
||||||
|
"dept": str or null,
|
||||||
|
"nom": str,
|
||||||
|
"prenom": str,
|
||||||
|
"active":bool
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = 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)
|
||||||
|
if dest_dept is not False:
|
||||||
|
if dest_dept == "@all":
|
||||||
|
dest_dept = None
|
||||||
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
||||||
|
if (None not in allowed_depts) and (
|
||||||
|
(orig_dept not in allowed_depts) or (dest_dept not in allowed_depts)
|
||||||
|
):
|
||||||
|
return error_response(403, "user_edit: departement non autorise")
|
||||||
|
if dest_dept != orig_dept:
|
||||||
|
if (dest_dept is not None) and (
|
||||||
|
Departement.query.filter_by(acronym=dest_dept).first() is None
|
||||||
|
):
|
||||||
|
return error_response(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))
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(user.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/user/<int:uid>/role/<string:role_name>/add", methods=["POST"])
|
||||||
|
@api_web_bp.route("/user/<int:uid>/role/<string:role_name>/add", methods=["POST"])
|
||||||
|
@bp.route(
|
||||||
|
"/user/<int:uid>/role/<string:role_name>/add/departement/<string:dept>",
|
||||||
|
methods=["POST"],
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/user/<int:uid>/role/<string:role_name>/add/departement/<string:dept>",
|
||||||
|
methods=["POST"],
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
|
def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||||
|
"""Add a role to the user"""
|
||||||
|
user: User = User.query.get_or_404(uid)
|
||||||
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
|
if dept is not None: # check
|
||||||
|
_ = Departement.query.filter_by(acronym=dept).first_or_404()
|
||||||
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoSuperAdmin)
|
||||||
|
if (None not in allowed_depts) and (dept not in allowed_depts):
|
||||||
|
return error_response(403, "user_role_add: departement non autorise")
|
||||||
|
user.add_role(role, dept)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(user.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/user/<int:uid>/role/<string:role_name>/remove", methods=["POST"])
|
||||||
|
@api_web_bp.route("/user/<int:uid>/role/<string:role_name>/remove", methods=["POST"])
|
||||||
|
@bp.route(
|
||||||
|
"/user/<int:uid>/role/<string:role_name>/remove/departement/<string:dept>",
|
||||||
|
methods=["POST"],
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/user/<int:uid>/role/<string:role_name>/remove/departement/<string:dept>",
|
||||||
|
methods=["POST"],
|
||||||
|
)
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoSuperAdmin)
|
||||||
|
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||||
|
"""Remove the role from the user"""
|
||||||
|
user: User = User.query.get_or_404(uid)
|
||||||
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||||
|
if dept is not None: # check
|
||||||
|
_ = Departement.query.filter_by(acronym=dept).first_or_404()
|
||||||
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoSuperAdmin)
|
||||||
|
if (None not in allowed_depts) and (dept not in allowed_depts):
|
||||||
|
return error_response(403, "user_role_remove: departement non autorise")
|
||||||
|
|
||||||
|
query = UserRole.query.filter(UserRole.role == role, UserRole.user == user)
|
||||||
|
if dept is not None:
|
||||||
|
query = query.filter(UserRole.dept == dept)
|
||||||
|
user_role = query.first()
|
||||||
|
if user_role:
|
||||||
|
db.session.delete(user_role)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(user.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/roles")
|
||||||
|
@api_web_bp.route("/roles")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def list_roles():
|
||||||
|
"""Tous les rôles définis"""
|
||||||
|
return jsonify([role.to_dict() for role in Role.query])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/permissions")
|
||||||
|
@api_web_bp.route("/permissions")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def list_permissions():
|
||||||
|
"""Liste des noms de permissions définies"""
|
||||||
|
return jsonify(list(Permission.permission_by_name.keys()))
|
||||||
|
@ -428,6 +428,14 @@ class Role(db.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}: perm={', '.join(Permission.permissions_names(self.permissions))}"
|
return f"{self.name}: perm={', '.join(Permission.permissions_names(self.permissions))}"
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"As dict. Convert permissions to names."
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"name": self.name,
|
||||||
|
"permissions": Permission.permissions_names(self.permissions),
|
||||||
|
}
|
||||||
|
|
||||||
def add_permission(self, perm):
|
def add_permission(self, perm):
|
||||||
self.permissions |= perm
|
self.permissions |= perm
|
||||||
|
|
||||||
|
@ -600,22 +600,6 @@ def float_null_is_null(x):
|
|||||||
return float(x)
|
return float(x)
|
||||||
|
|
||||||
|
|
||||||
BOOL_STR = {
|
|
||||||
"": False,
|
|
||||||
"false": False,
|
|
||||||
"0": False,
|
|
||||||
"1": True,
|
|
||||||
"true": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def bool_or_str(x) -> bool:
|
|
||||||
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
|
|
||||||
if isinstance(x, str):
|
|
||||||
return BOOL_STR[x.lower()]
|
|
||||||
return bool(x)
|
|
||||||
|
|
||||||
|
|
||||||
# post filtering
|
# post filtering
|
||||||
#
|
#
|
||||||
def UniqListofDicts(L, key):
|
def UniqListofDicts(L, key):
|
||||||
|
@ -42,7 +42,6 @@ from app.scodoc.scolog import logdb
|
|||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_abs
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_find_etud
|
from app.scodoc import sco_find_etud
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
@ -807,7 +806,7 @@ def ListeAbsEtud(
|
|||||||
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005"
|
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005"
|
||||||
"""
|
"""
|
||||||
# si absjust_only, table absjust seule (export xls ou pdf)
|
# si absjust_only, table absjust seule (export xls ou pdf)
|
||||||
absjust_only = ndb.bool_or_str(absjust_only)
|
absjust_only = scu.to_bool(absjust_only)
|
||||||
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
|
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
|
||||||
etudid = etudid or False
|
etudid = etudid or False
|
||||||
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
||||||
|
@ -91,7 +91,7 @@ _ueEditor = ndb.EditableTable(
|
|||||||
sortkey="numero",
|
sortkey="numero",
|
||||||
input_formators={
|
input_formators={
|
||||||
"type": ndb.int_null_is_zero,
|
"type": ndb.int_null_is_zero,
|
||||||
"is_external": ndb.bool_or_str,
|
"is_external": scu.to_bool,
|
||||||
"ects": ndb.float_null_is_null,
|
"ects": ndb.float_null_is_null,
|
||||||
},
|
},
|
||||||
output_formators={
|
output_formators={
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
_SCO_PERMISSIONS = (
|
_SCO_PERMISSIONS = (
|
||||||
# permission bit, symbol, description
|
# permission bit, symbol, description
|
||||||
# ScoSuperAdmin est utilisé pour:
|
# ScoSuperAdmin est utilisé pour:
|
||||||
# - ZScoDoc: add/delete departments
|
# - add/delete departments
|
||||||
# - tous rôles lors creation utilisateurs
|
# - tous rôles lors creation utilisateurs
|
||||||
(1 << 1, "ScoSuperAdmin", "Super Administrateur"),
|
(1 << 1, "ScoSuperAdmin", "Super Administrateur"),
|
||||||
(1 << 2, "APIView", "Voir (obsolete, use ScoView)"), # deprecated
|
(1 << 2, "APIView", "Voir (obsolete, use ScoView)"), # deprecated
|
||||||
|
@ -638,6 +638,22 @@ def is_valid_filename(filename):
|
|||||||
return VALID_EXP.match(filename)
|
return VALID_EXP.match(filename)
|
||||||
|
|
||||||
|
|
||||||
|
BOOL_STR = {
|
||||||
|
"": False,
|
||||||
|
"false": False,
|
||||||
|
"0": False,
|
||||||
|
"1": True,
|
||||||
|
"true": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_bool(x) -> bool:
|
||||||
|
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
|
||||||
|
if isinstance(x, str):
|
||||||
|
return BOOL_STR.get(x.lower().strip(), True)
|
||||||
|
return bool(x)
|
||||||
|
|
||||||
|
|
||||||
def bul_filename_old(sem: dict, etud: dict, format):
|
def bul_filename_old(sem: dict, etud: dict, format):
|
||||||
"""Build a filename for this bulletin"""
|
"""Build a filename for this bulletin"""
|
||||||
dt = time.strftime("%Y-%m-%d")
|
dt = time.strftime("%Y-%m-%d")
|
||||||
|
@ -337,6 +337,7 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=
|
|||||||
user_role = UserRole.query.filter(
|
user_role = UserRole.query.filter(
|
||||||
UserRole.role == role, UserRole.user == user, UserRole.dept == dept_acronym
|
UserRole.role == role, UserRole.user == user, UserRole.dept == dept_acronym
|
||||||
).first()
|
).first()
|
||||||
|
if user_role:
|
||||||
db.session.delete(user_role)
|
db.session.delete(user_role)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -352,8 +353,8 @@ def abort_if_false(ctx, param, value):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
callback=abort_if_false,
|
callback=abort_if_false,
|
||||||
expose_value=False,
|
expose_value=False,
|
||||||
prompt=f"""Attention: Cela va effacer toutes les données du département
|
prompt="""Attention: Cela va effacer toutes les données du département
|
||||||
(étudiants, notes, formations, etc)
|
(étudiants, notes, formations, etc).
|
||||||
Voulez-vous vraiment continuer ?
|
Voulez-vous vraiment continuer ?
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
@ -63,3 +63,45 @@ def test_list_users(api_admin_headers):
|
|||||||
# (test, other et u_TAPI)
|
# (test, other et u_TAPI)
|
||||||
# plus l'utilisateur de chaque département jusqu'au leur
|
# plus l'utilisateur de chaque département jusqu'au leur
|
||||||
# (u_AA voit AA, u_BB voit AA et BB, etc)
|
# (u_AA voit AA, u_BB voit AA et BB, etc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_users(api_admin_headers):
|
||||||
|
"""
|
||||||
|
Routes: /user/create
|
||||||
|
/user/edit/<int:uid>
|
||||||
|
"""
|
||||||
|
admin_h = api_admin_headers
|
||||||
|
nb_users = len(GET("/users/query", headers=admin_h))
|
||||||
|
user = POST_JSON(
|
||||||
|
"/user/create",
|
||||||
|
{"user_name": "toto", "nom": "Toto"},
|
||||||
|
headers=admin_h,
|
||||||
|
)
|
||||||
|
assert user["user_name"] == "toto"
|
||||||
|
assert user["dept"] is None
|
||||||
|
assert user["active"] is True
|
||||||
|
assert nb_users == 1 + len(GET("/users/query", headers=admin_h))
|
||||||
|
# Change le dept et rend inactif
|
||||||
|
user = POST_JSON(
|
||||||
|
f"/user/edit/{user['id']}",
|
||||||
|
{"active": False, "dept": "TAPI"},
|
||||||
|
headers=admin_h,
|
||||||
|
)
|
||||||
|
assert user["dept"] is "TAPI"
|
||||||
|
assert user["active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_roles(api_admin_headers):
|
||||||
|
"""
|
||||||
|
Routes: /user/create
|
||||||
|
/user/edit/<int:uid>
|
||||||
|
"""
|
||||||
|
admin_h = api_admin_headers
|
||||||
|
user = POST_JSON(
|
||||||
|
"/user/create",
|
||||||
|
{"user_name": "test_roles", "nom": "Role", "prenom": "Test"},
|
||||||
|
headers=admin_h,
|
||||||
|
)
|
||||||
|
uid = user["id"]
|
||||||
|
ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h)
|
||||||
|
assert ans["user_name"] == "test_roles"
|
||||||
|
Loading…
Reference in New Issue
Block a user