diff --git a/app/api/users.py b/app/api/users.py
index 4fe895107a..3bbc96d338 100644
--- a/app/api/users.py
+++ b/app/api/users.py
@@ -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()
diff --git a/app/auth/models.py b/app/auth/models.py
index dcf762a2d1..38bad09742 100644
--- a/app/auth/models.py
+++ b/app/auth/models.py
@@ -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:
diff --git a/app/models/__init__.py b/app/models/__init__.py
index c4e04bd6b7..76967ed429 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -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)
diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py
index a102700a54..fad28632e3 100644
--- a/app/scodoc/sco_users.py
+++ b/app/scodoc/sco_users.py
@@ -102,7 +102,7 @@ def index_html(
{menu_roles}
-
+
"""
)
@@ -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",
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 0f9072c81c..f96efa8b4b 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -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);
-}
diff --git a/app/templates/auth/user_info_page.j2 b/app/templates/auth/user_info_page.j2
index 8a6cb7f6e4..e21da1650c 100644
--- a/app/templates/auth/user_info_page.j2
+++ b/app/templates/auth/user_info_page.j2
@@ -18,6 +18,7 @@
Prénom : {{user.prenom or ""}}
Mail : {{user.email}}
Mail institutionnel: {{user.email_institutionnel or ""}}
+ Identifiant EDT: {{user.edt_id or ""}}
Rôles : {{user.get_roles_string()}}
Dept : {{user.dept or ""}}
{% if user.passwd_temp or user.password_scodoc7 %}
diff --git a/app/views/users.py b/app/views/users.py
index 3d07b05a3f..3f360c458b 100644
--- a/app/views/users.py
+++ b/app/views/users.py
@@ -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 += [
diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py
index 1421a29cf4..7785655392 100644
--- a/tests/api/test_api_users.py
+++ b/tests/api/test_api_users.py
@@ -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):
diff --git a/tests/unit/test_users.py b/tests/unit/test_users.py
index 789ebc5ca9..8f4d8a84fe 100644
--- a/tests/unit/test_users.py
+++ b/tests/unit/test_users.py
@@ -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()