forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 %}
|
||||
|
@ -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 += [
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user