Compare commits

...

19 Commits

Author SHA1 Message Date
5cd495e33e ajout export fichier par NIP 2023-12-03 00:17:16 +01:00
a5e5ad6248 Améliorations partielles de SignaleAssiduiteEtud 2023-11-29 18:07:14 +01:00
7cda427cac Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc 2023-11-29 17:15:27 +01:00
a0316c22e7 Fix: identite.etat_civil 2023-11-29 17:13:21 +01:00
2d3490e7fa Fix: nom ref. comp. BUT Info-Comm. 2023-11-28 17:16:40 +01:00
dcea12f6fc Table etud courants + export xlsx, + code nip 2023-11-26 19:04:03 +01:00
568c8681ba Accepte codage boursiers O/N (USPN), fonction de resynchro globale, table de tous les étudiants courants avec état boursier. 2023-11-26 18:28:56 +01:00
Iziram
cbb11d0e8e Merge branch 'liste_assi' into main96 2023-11-24 18:08:12 +01:00
Iziram
7a80ec3ce5 Assiduites : Tableaux (sans QOL) 2023-11-24 18:07:30 +01:00
Iziram
b13e751e1a Assiduites : WIP tableaux actions (sauf modifier) 2023-11-24 13:58:03 +01:00
60109bb513 Import etudiant: accepte boursier=O|N (API USPN) 2023-11-24 13:55:53 +01:00
e634b50d56 API: create/edit etudiant, admission, adresse 2023-11-23 17:08:18 +01:00
2377918b54 API: etudiant/create (WIP), refactoring. 2023-11-22 23:31:16 +01:00
532fb3e701 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc 2023-11-22 17:59:10 +01:00
457a9ddf51 Améliore code et tests gestion User 2023-11-22 17:55:15 +01:00
ea1a03a654 API: enrichit création/édition GroupDescr 2023-11-22 17:54:16 +01:00
e41879a1e1 Upgrade ReportLab to 4.0.7 2023-11-22 16:23:07 +01:00
4d3cbf7e75 API: enrichit création/édition User 2023-11-21 22:28:50 +01:00
939371cff9 complete message aide 2023-11-21 12:19:35 +01:00
65 changed files with 2178 additions and 792 deletions

View File

@ -7,7 +7,7 @@
""" """
ScoDoc 9 API : accès aux départements ScoDoc 9 API : accès aux départements
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/), Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api). mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
""" """
from datetime import datetime from datetime import datetime
@ -271,23 +271,11 @@ def dept_formsemestres_courants(acronym: str):
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
date_courante = request.args.get("date_courante") date_courante = request.args.get("date_courante")
if date_courante: date_courante = datetime.fromisoformat(date_courante) if date_courante else None
test_date = datetime.fromisoformat(date_courante)
else:
test_date = app.db.func.now()
# Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id,
FormSemestre.date_debut <= test_date,
FormSemestre.date_fin >= test_date,
)
return [ return [
d.to_dict_api() formsemestre.to_dict_api()
for d in formsemestres.order_by( for formsemestre in FormSemestre.get_dept_formsemestres_courants(
FormSemestre.date_debut.desc(), dept, date_courante
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
) )
] ]

View File

@ -18,20 +18,24 @@ from sqlalchemy import desc, func, or_
from sqlalchemy.dialects.postgresql import VARCHAR from sqlalchemy.dialects.postgresql import VARCHAR
import app import app
from app import db
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.api import tools from app.api import tools
from app.but import bulletin_but_court from app.but import bulletin_but_court
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import ( from app.models import (
Admission, Admission,
Adresse,
Departement, Departement,
FormSemestreInscription, FormSemestreInscription,
FormSemestre, FormSemestre,
Identite, Identite,
ScolarNews,
) )
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc import sco_etud
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error, suppress_accents from app.scodoc.sco_utils import json_error, suppress_accents
@ -475,3 +479,112 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
data = sco_groups.get_etud_groups(etud.id, formsemestre.id) data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
return data return data
@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
@scodoc
@permission_required(Permission.EtudInscrit)
@as_json
def etudiant_create(force=False):
"""Création d'un nouvel étudiant
Si force, crée même si homonymie détectée.
L'étudiant créé n'est pas inscrit à un semestre.
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
"""
args = request.get_json(force=True) # may raise 400 Bad Request
dept = args.get("dept", None)
if not dept:
return scu.json_error(400, "dept requis")
dept_o = Departement.query.filter_by(acronym=dept).first()
if not dept_o:
return scu.json_error(400, "dept invalide")
app.set_sco_dept(dept)
args["dept_id"] = dept_o.id
# vérifie que le département de création est bien autorisé
if not current_user.has_permission(Permission.EtudInscrit, dept):
return json_error(403, "departement non autorisé")
nom = args.get("nom", None)
prenom = args.get("prenom", None)
ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
if not ok:
return scu.json_error(400, "nom ou prénom invalide")
if len(homonyms) > 0 and not force:
return scu.json_error(
400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
)
etud = Identite.create_etud(**args)
db.session.flush()
# --- Données admission
admission_args = args.get("admission", None)
if admission_args:
etud.admission.from_dict(admission_args)
# --- Adresse
adresses = args.get("adresses", [])
if adresses:
# ne prend en compte que la première adresse
# car si la base est concue pour avoir plusieurs adresses par étudiant,
# l'application n'en gère plus qu'une seule.
adresse = etud.adresses.first()
adresse.from_dict(adresses[0])
# Poste une nouvelle dans le département concerné:
ScolarNews.add(
typ=ScolarNews.NEWS_INSCR,
text=f"Nouvel étudiant {etud.html_link_fiche()}",
url=etud.url_fiche(),
max_frequency=0,
dept_id=dept_o.id,
)
db.session.commit()
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
db.session.refresh(etud)
r = etud.to_dict_api()
return r
@bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
@scodoc
@permission_required(Permission.EtudInscrit)
def etudiant_edit(
code_type: str = "etudid",
code: str = None,
):
"""Edition des données étudiant (identité, admission, adresses)"""
if code_type == "nip":
query = Identite.query.filter_by(code_nip=code)
elif code_type == "etudid":
try:
etudid = int(code)
except ValueError:
return json_error(404, "invalid etudid type")
query = Identite.query.filter_by(id=etudid)
elif code_type == "ine":
query = Identite.query.filter_by(code_ine=code)
else:
return json_error(404, "invalid code_type")
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
etud: Identite = query.first()
#
args = request.get_json(force=True) # may raise 400 Bad Request
etud.from_dict(args)
admission_args = args.get("admission", None)
if admission_args:
etud.admission.from_dict(admission_args)
# --- Adresse
adresses = args.get("adresses", [])
if adresses:
# ne prend en compte que la première adresse
# car si la base est concue pour avoir plusieurs adresses par étudiant,
# l'application n'en gère plus qu'une seule.
adresse = etud.adresses.first()
adresse.from_dict(adresses[0])
db.session.commit()
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
db.session.refresh(etud)
r = etud.to_dict_api()
return r

View File

@ -646,8 +646,8 @@ def justif_import(justif_id: int = None):
return json_error(404, err.args[0]) return json_error(404, err.args[0])
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"]) @bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"])
@api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"]) @api_web_bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["GET", "POST"])
@scodoc @scodoc
@login_required @login_required
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)

View File

@ -303,15 +303,19 @@ def group_create(partition_id: int): # partition-group-create
return json_error(403, "partition non editable") return json_error(403, "partition non editable")
if not partition.formsemestre.can_change_groups(): if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(401, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name")
if group_name is None:
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
if not GroupDescr.check_name(partition, group_name):
return json_error(API_CLIENT_ERROR, "invalid group_name")
group_name = group_name.strip()
group = GroupDescr(group_name=group_name, partition_id=partition_id) args = request.get_json(force=True) # may raise 400 Bad Request
group_name = args.get("group_name")
if not isinstance(group_name, str):
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
args["group_name"] = args["group_name"].strip()
if not GroupDescr.check_name(partition, args["group_name"]):
return json_error(API_CLIENT_ERROR, "invalid group_name")
args["partition_id"] = partition_id
try:
group = GroupDescr(**args)
except TypeError:
return json_error(API_CLIENT_ERROR, "invalid arguments")
db.session.add(group) db.session.add(group)
db.session.commit() db.session.commit()
log(f"created group {group}") log(f"created group {group}")
@ -369,16 +373,22 @@ def group_edit(group_id: int):
return json_error(403, "partition non editable") return json_error(403, "partition non editable")
if not group.partition.formsemestre.can_change_groups(): if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(401, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name") args = request.get_json(force=True) # may raise 400 Bad Request
if group_name is not None: if "group_name" in args:
group_name = group_name.strip() if not isinstance(args["group_name"], str):
if not GroupDescr.check_name(group.partition, group_name, existing=True): return json_error(API_CLIENT_ERROR, "invalid data format for group_name")
args["group_name"] = args["group_name"].strip() if args["group_name"] else ""
if not GroupDescr.check_name(
group.partition, args["group_name"], existing=True
):
return json_error(API_CLIENT_ERROR, "invalid group_name") return json_error(API_CLIENT_ERROR, "invalid group_name")
group.group_name = group_name
db.session.add(group) group.from_dict(args)
db.session.commit() db.session.add(group)
log(f"modified {group}") db.session.commit()
log(f"modified {group}")
app.set_sco_dept(group.partition.formsemestre.departement.acronym) app.set_sco_dept(group.partition.formsemestre.departement.acronym)
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
return group.to_dict(with_partition=True) return group.to_dict(with_partition=True)

View File

@ -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_name=user_name)
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()

View File

@ -12,7 +12,6 @@ from typing import Optional
import cracklib # pylint: disable=import-error import cracklib # pylint: disable=import-error
import flask
from flask import current_app, g from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
@ -21,14 +20,13 @@ 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
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_etud # a deplacer dans scu
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$") VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
@ -53,13 +51,14 @@ def is_valid_password(cleartxt) -> bool:
def invalid_user_name(user_name: str) -> bool: def invalid_user_name(user_name: str) -> bool:
"Check that user_name (aka login) is invalid" "Check that user_name (aka login) is invalid"
return ( return (
(len(user_name) < 2) not user_name
or (len(user_name) < 2)
or (len(user_name) >= USERNAME_STR_LEN) or (len(user_name) >= USERNAME_STR_LEN)
or not VALID_LOGIN_EXP.match(user_name) or not VALID_LOGIN_EXP.match(user_name)
) )
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)
@ -116,12 +115,17 @@ class User(UserMixin, db.Model):
) )
def __init__(self, **kwargs): def __init__(self, **kwargs):
"user_name:str is mandatory"
self.roles = [] self.roles = []
self.user_roles = [] self.user_roles = []
# check login: # check login:
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]): if not "user_name" in kwargs:
raise ValueError("missing user_name argument")
if 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) kwargs["nom"] = kwargs.get("nom", "") or ""
kwargs["prenom"] = kwargs.get("prenom", "") or ""
super().__init__(**kwargs)
# Ajoute roles: # Ajoute roles:
if ( if (
not self.roles not self.roles
@ -251,12 +255,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,37 +275,50 @@ 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
# Dates
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
)
# booléens:
for field in ("active", "cas_allow_login", "cas_allow_scodoc_login"):
if field in args:
args_dict[field] = scu.to_bool(args.get(field))
# chaines ne devant pas être NULLs
for field in ("nom", "prenom"):
if field in args:
args[field] = args[field] or ""
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_string : roles, encoded like "Ens_RT, Secr_CJ"
- date_expiration is a dateime object.
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)))
if new_user: if new_user:
if "user_name" in data: if "user_name" in data:
# never change name of existing users # never change name of existing users
if invalid_user_name(data["user_name"]):
raise ValueError(f"invalid user_name: {data['user_name']}")
self.user_name = data["user_name"] self.user_name = data["user_name"]
if "password" in data: if "password" in data:
self.set_password(data["password"]) self.set_password(data["password"])
if invalid_user_name(self.user_name):
raise ValueError(f"invalid user_name: {self.user_name}")
# Roles: roles_string is "Ens_RT, Secr_RT, ..." # Roles: roles_string is "Ens_RT, Secr_RT, ..."
if "roles_string" in data: if "roles_string" in data:
self.user_roles = [] self.user_roles = []
@ -309,6 +327,8 @@ class User(UserMixin, db.Model):
role, dept = UserRole.role_dept_from_string(r_d) role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept) self.add_role(role, dept)
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
# Set cas_id using regexp if configured: # Set cas_id using regexp if configured:
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp") exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
if exp and self.email_institutionnel: if exp and self.email_institutionnel:
@ -441,8 +461,8 @@ class User(UserMixin, db.Model):
"""nomplogin est le nom en majuscules suivi du prénom et du login """nomplogin est le nom en majuscules suivi du prénom et du login
e.g. Dupont Pierre (dupont) e.g. Dupont Pierre (dupont)
""" """
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper() nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})" return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
@staticmethod @staticmethod
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]: def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
@ -460,29 +480,29 @@ class User(UserMixin, db.Model):
def get_nom_fmt(self): def get_nom_fmt(self):
"""Nom formaté: "Martin" """ """Nom formaté: "Martin" """
if self.nom: if self.nom:
return sco_etud.format_nom(self.nom, uppercase=False) return scu.format_nom(self.nom, uppercase=False)
else: else:
return self.user_name return self.user_name
def get_prenom_fmt(self): def get_prenom_fmt(self):
"""Prénom formaté (minuscule capitalisées)""" """Prénom formaté (minuscule capitalisées)"""
return sco_etud.format_prenom(self.prenom) return scu.format_prenom(self.prenom)
def get_nomprenom(self): def get_nomprenom(self):
"""Nom capitalisé suivi de l'initiale du prénom: """Nom capitalisé suivi de l'initiale du prénom:
Viennet E. Viennet E.
""" """
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom)) prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
return (self.get_nom_fmt() + " " + prenom_abbrv).strip() return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
def get_prenomnom(self): def get_prenomnom(self):
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """ """L'initiale du prénom suivie du nom: "J.-C. Dupont" """
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom)) prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
return (prenom_abbrv + " " + self.get_nom_fmt()).strip() return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
def get_nomcomplet(self): def get_nomcomplet(self):
"Prénom et nom complets" "Prénom et nom complets"
return sco_etud.format_prenom(self.prenom) + " " + self.get_nom_fmt() return scu.format_prenom(self.prenom) + " " + self.get_nom_fmt()
# nomnoacc était le nom en minuscules sans accents (inutile) # nomnoacc était le nom en minuscules sans accents (inutile)

View File

@ -6,6 +6,7 @@ from flask import Blueprint
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.auth.models import User from app.auth.models import User
from app.models import Departement from app.models import Departement
import app.scodoc.sco_utils as scu
bp = Blueprint("entreprises", __name__) bp = Blueprint("entreprises", __name__)
@ -15,12 +16,12 @@ SIRET_PROVISOIRE_START = "xx"
@bp.app_template_filter() @bp.app_template_filter()
def format_prenom(s): def format_prenom(s):
return sco_etud.format_prenom(s) return scu.format_prenom(s)
@bp.app_template_filter() @bp.app_template_filter()
def format_nom(s): def format_nom(s):
return sco_etud.format_nom(s) return scu.format_nom(s)
@bp.app_template_filter() @bp.app_template_filter()

View File

@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
) )
) )
elif request.method == "GET": elif request.method == "GET":
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} { form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
sco_etud.format_prenom(etudiant.prenom)}""" scu.format_prenom(etudiant.prenom)}"""
form.etudid.data = etudiant.id form.etudid.data = etudiant.id
form.type_offre.data = stage_apprentissage.type_offre form.type_offre.data = stage_apprentissage.type_offre
form.date_debut.data = stage_apprentissage.date_debut form.date_debut.data = stage_apprentissage.date_debut
@ -1699,7 +1699,7 @@ def json_etudiants():
list = [] list = []
for etudiant in etudiants: for etudiant in etudiants:
content = {} content = {}
value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}" value = f"{scu.format_nom(etudiant.nom)} {scu.format_prenom(etudiant.prenom)}"
if etudiant.inscription_courante() is not None: if etudiant.inscription_courante() is not None:
content = { content = {
"id": f"{etudiant.id}", "id": f"{etudiant.id}",

View File

@ -53,8 +53,9 @@ class ScoDocModel:
@classmethod @classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict: def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded. """Returns a copy of dict with only the keys belonging to the Model and not in excluded.
By default, excluded == { 'id' }""" Add 'id' to excluded."""
excluded = {"id"} if excluded is None else set() excluded = excluded or set()
excluded.add("id") # always exclude id
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id) # Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
my_attributes = [ my_attributes = [
a a
@ -70,7 +71,7 @@ class ScoDocModel:
@classmethod @classmethod
def convert_dict_fields(cls, args: dict) -> dict: def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict. No side effect. """Convert fields from the given dict to model's attributes values. No side effect.
By default, do nothing, but is overloaded by some subclasses. By default, do nothing, but is overloaded by some subclasses.
args: dict with args in application. args: dict with args in application.
returns: dict to store in model's db. returns: dict to store in model's db.
@ -78,9 +79,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 = 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)
@ -130,7 +133,6 @@ from app.models.notes import (
NotesNotesLog, NotesNotesLog,
) )
from app.models.validations import ( from app.models.validations import (
ScolarEvent,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
ScolarAutorisationInscription, ScolarAutorisationInscription,
) )
@ -149,3 +151,4 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.models.assiduites import Assiduite, Justificatif from app.models.assiduites import Assiduite, Justificatif
from app.models.scolar_event import ScolarEvent

View File

@ -3,8 +3,8 @@
""" """
from datetime import datetime from datetime import datetime
from app import db, log from app import db, log, g
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription from app.models import ModuleImpl, Module, Scolog, FormSemestre, FormSemestreInscription
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.auth.models import User from app.auth.models import User
from app.scodoc import sco_abs_notification from app.scodoc import sco_abs_notification
@ -204,6 +204,77 @@ class Assiduite(db.Model):
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut) sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
return nouv_assiduite return nouv_assiduite
def set_moduleimpl(self, moduleimpl_id: int | str) -> bool:
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
if moduleimpl is not None:
# Vérification de l'inscription de l'étudiant
if moduleimpl.est_inscrit(self.etudiant):
self.moduleimpl_id = moduleimpl.id
else:
raise ScoValueError("L'étudiant n'est pas inscrit au module")
elif isinstance(moduleimpl_id, str):
if self.external_data is None:
self.external_data = {"module": moduleimpl_id}
else:
self.external_data["module"] = moduleimpl_id
self.moduleimpl_id = None
else:
# Vérification si module forcé
formsemestre: FormSemestre = get_formsemestre_from_data(
{
"etudid": self.etudid,
"date_debut": self.date_debut,
"date_fin": self.date_fin,
}
)
force: bool
if formsemestre:
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
else:
force = is_assiduites_module_forced(dept_id=etud.dept_id)
if force:
raise ScoValueError("Module non renseigné")
return True
def supprimer(self):
from app.scodoc import sco_assiduites as scass
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
# route sans département
set_sco_dept(self.etudiant.departement.acronym)
obj_dict: dict = self.to_dict()
# Suppression de l'objet et LOG
log(f"delete_assidutite: {self.etudiant.id} {self}")
Scolog.logdb(
method=f"delete_assiduite",
etudid=self.etudiant.id,
msg=f"Assiduité: {self}",
)
db.session.delete(self)
# Invalidation du cache
scass.simple_invalidate_cache(obj_dict)
def get_formsemestre(self) -> FormSemestre:
return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str:
if self.moduleimpl_id is not None:
if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
mod: Module = Module.query.get(modimpl.module_id)
return f"{mod.code} {mod.titre}"
elif self.external_data is not None and "module" in self.external_data:
return (
"Tout module"
if self.external_data["module"] == "Autre"
else self.external_data["module"]
)
return "Non spécifié" if traduire else None
class Justificatif(db.Model): class Justificatif(db.Model):
""" """
@ -334,6 +405,39 @@ class Justificatif(db.Model):
) )
return nouv_justificatif return nouv_justificatif
def supprimer(self):
from app.scodoc import sco_assiduites as scass
# Récupération de l'archive du justificatif
archive_name: str = self.fichier
if archive_name is not None:
# Si elle existe : on essaye de la supprimer
archiver: JustificatifArchiver = JustificatifArchiver()
try:
archiver.delete_justificatif(self.etudiant, archive_name)
except ValueError:
pass
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
# route sans département
set_sco_dept(self.etudiant.departement.acronym)
# On invalide le cache
scass.simple_invalidate_cache(self.to_dict())
# Suppression de l'objet et LOG
log(f"delete_justificatif: {self.etudiant.id} {self}")
Scolog.logdb(
method=f"delete_justificatif",
etudid=self.etudiant.id,
msg=f"Justificatif: {self}",
)
db.session.delete(self)
# On actualise les assiduités justifiées de l'étudiant concerné
compute_assiduites_justified(
self.etudid,
Justificatif.query.filter_by(etudid=self.etudid).all(),
True,
)
def is_period_conflicting( def is_period_conflicting(
date_debut: datetime, date_debut: datetime,

View File

@ -15,7 +15,8 @@ from sqlalchemy import desc, text
from app import db, log from app import db, log
from app import models from app import models
from app.models.departements import Departement
from app.models.scolar_event import ScolarEvent
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat from app.scodoc.sco_bac import Baccalaureat
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
@ -170,9 +171,13 @@ class Identite(db.Model, models.ScoDocModel):
def html_link_fiche(self) -> str: def html_link_fiche(self) -> str:
"lien vers la fiche" "lien vers la fiche"
return f"""<a class="stdlink" href="{ return f"""<a class="stdlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
}">{self.nomprenom}</a>""" def url_fiche(self) -> str:
"url de la fiche étudiant"
return url_for(
"scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id
)
@classmethod @classmethod
def from_request(cls, etudid=None, code_nip=None) -> "Identite": def from_request(cls, etudid=None, code_nip=None) -> "Identite":
@ -200,19 +205,39 @@ class Identite(db.Model, models.ScoDocModel):
return cls.create_from_dict(args) return cls.create_from_dict(args)
@classmethod @classmethod
def create_from_dict(cls, data) -> "Identite": def create_from_dict(cls, args) -> "Identite":
"""Crée un étudiant à partir d'un dict, avec admission et adresse vides. """Crée un étudiant à partir d'un dict, avec admission et adresse vides.
If required dept_id or dept are not specified, set it to the current dept.
args: dict with args in application.
Les clés adresses et admission ne SONT PAS utilisées.
(added to session but not flushed nor commited) (added to session but not flushed nor commited)
""" """
etud: Identite = super(cls, cls).create_from_dict(data) if not "dept_id" in args:
if (data.get("admission_id", None) is None) and ( if "dept" in args:
data.get("admission", None) is None departement = Departement.query.filter_by(acronym=args["dept"]).first()
): if departement:
args["dept_id"] = departement.id
if not "dept_id" in args:
args["dept_id"] = g.scodoc_dept_id
etud: Identite = super().create_from_dict(args)
if args.get("admission_id", None) is None:
etud.admission = Admission() etud.admission = Admission()
etud.adresses.append(Adresse(typeadresse="domicile")) etud.adresses.append(Adresse(typeadresse="domicile"))
db.session.flush() db.session.flush()
event = ScolarEvent(etud=etud, event_type="CREATION")
db.session.add(event)
log(f"Identite.create {etud}")
return etud return etud
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded."""
return super().filter_model_attributes(
data,
excluded=(excluded or set()) | {"adresses", "admission", "departement"},
)
@property @property
def civilite_str(self) -> str: def civilite_str(self) -> str:
"""returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre, """returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre,
@ -259,14 +284,13 @@ class Identite(db.Model, models.ScoDocModel):
def nomprenom(self, reverse=False) -> str: def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont" """Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité. Si reverse, "Dupont Pierre", sans civilité.
Prend l'identité courant et non celle de l'état civile si elles diffèrent.
""" """
nom = self.nom_usuel or self.nom nom = self.nom_usuel or self.nom
prenom = self.prenom_str prenom = self.prenom_str
if reverse: if reverse:
fields = (nom, prenom) return f"{nom} {prenom}".strip()
else: return f"{self.civilite_str} {prenom} {nom}".strip()
fields = (self.civilite_str, prenom, nom)
return " ".join([x for x in fields if x])
@property @property
def prenom_str(self): def prenom_str(self):
@ -282,12 +306,10 @@ class Identite(db.Model, models.ScoDocModel):
@property @property
def etat_civil(self) -> str: def etat_civil(self) -> str:
"M. Prénom NOM, utilisant les données état civil si présentes, usuelles sinon." "M. PRÉNOM NOM, utilisant les données état civil si présentes, usuelles sinon."
if self.prenom_etat_civil: return f"""{self.civilite_etat_civil_str} {
civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil] self.prenom_etat_civil or self.prenom or ''} {
return f"{civ} {self.prenom_etat_civil} {self.nom}" self.nom or ''}""".strip()
else:
return self.nomprenom
@property @property
def nom_short(self): def nom_short(self):
@ -321,8 +343,6 @@ class Identite(db.Model, models.ScoDocModel):
@classmethod @classmethod
def convert_dict_fields(cls, args: dict) -> dict: def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict. No other side effect. """Convert fields in the given dict. No other side effect.
If required dept_id is not specified, set it to the current dept.
args: dict with args in application.
returns: dict to store in model's db. returns: dict to store in model's db.
""" """
# Les champs qui sont toujours stockés en majuscules: # Les champs qui sont toujours stockés en majuscules:
@ -341,8 +361,6 @@ class Identite(db.Model, models.ScoDocModel):
"code_ine", "code_ine",
} }
args_dict = {} args_dict = {}
if not "dept_id" in args:
args["dept_id"] = g.scodoc_dept_id
for key, value in args.items(): for key, value in args.items():
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property): if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
# compat scodoc7 (mauvaise idée de l'époque) # compat scodoc7 (mauvaise idée de l'époque)
@ -355,7 +373,7 @@ class Identite(db.Model, models.ScoDocModel):
elif key == "civilite_etat_civil": elif key == "civilite_etat_civil":
value = input_civilite_etat_civil(value) value = input_civilite_etat_civil(value)
elif key == "boursier": elif key == "boursier":
value = bool(value) value = scu.to_bool(value)
elif key == "date_naissance": elif key == "date_naissance":
value = ndb.DateDMYtoISO(value) value = ndb.DateDMYtoISO(value)
args_dict[key] = value args_dict[key] = value

View File

@ -133,7 +133,7 @@ class ScolarNews(db.Model):
return query.order_by(cls.date.desc()).limit(n).all() return query.order_by(cls.date.desc()).limit(n).all()
@classmethod @classmethod
def add(cls, typ, obj=None, text="", url=None, max_frequency=600): def add(cls, typ, obj=None, text="", url=None, max_frequency=600, dept_id=None):
"""Enregistre une nouvelle """Enregistre une nouvelle
Si max_frequency, ne génère pas 2 nouvelles "identiques" Si max_frequency, ne génère pas 2 nouvelles "identiques"
à moins de max_frequency secondes d'intervalle (10 minutes par défaut). à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
@ -141,10 +141,11 @@ class ScolarNews(db.Model):
même (obj, typ, user). même (obj, typ, user).
La nouvelle enregistrée est aussi envoyée par mail. La nouvelle enregistrée est aussi envoyée par mail.
""" """
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
if max_frequency: if max_frequency:
last_news = ( last_news = (
cls.query.filter_by( cls.query.filter_by(
dept_id=g.scodoc_dept_id, dept_id=dept_id,
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
type=typ, type=typ,
object=obj, object=obj,
@ -163,7 +164,7 @@ class ScolarNews(db.Model):
return return
news = ScolarNews( news = ScolarNews(
dept_id=g.scodoc_dept_id, dept_id=dept_id,
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
type=typ, type=typ,
object=obj, object=obj,

View File

@ -30,6 +30,7 @@ from app.models.but_refcomp import (
parcours_formsemestre, parcours_formsemestre,
) )
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.models.formations import Formation from app.models.formations import Formation
@ -521,7 +522,7 @@ class FormSemestre(db.Model):
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2, mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
jour_pivot_annee=1, jour_pivot_annee=1,
jour_pivot_periode=1, jour_pivot_periode=1,
): ) -> tuple[int, int]:
"""Calcule la session associée à un formsemestre commençant en date_debut """Calcule la session associée à un formsemestre commençant en date_debut
sous la forme (année, période) sous la forme (année, période)
année: première année de l'année scolaire année: première année de l'année scolaire
@ -571,6 +572,26 @@ class FormSemestre(db.Model):
mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(), mois_pivot_periode=ScoDocSiteConfig.get_month_debut_periode2(),
) )
@classmethod
def get_dept_formsemestres_courants(
cls, dept: Departement, date_courante: datetime.datetime | None = None
) -> db.Query:
"""Liste (query) ordonnée des formsemestres courants, c'est
à dire contenant la date courant (si None, la date actuelle)"""
date_courante = date_courante or db.func.now()
# Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id,
FormSemestre.date_debut <= date_courante,
FormSemestre.date_fin >= date_courante,
)
return formsemestres.order_by(
FormSemestre.date_debut.desc(),
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
)
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]: def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
"Liste des vdis" "Liste des vdis"
# was read_formsemestre_etapes # was read_formsemestre_etapes

View File

@ -11,14 +11,14 @@ from operator import attrgetter
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from app import db, log from app import db, log
from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN from app.models import ScoDocModel, Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
class Partition(db.Model): class Partition(db.Model, ScoDocModel):
"""Partition: découpage d'une promotion en groupes""" """Partition: découpage d'une promotion en groupes"""
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),) __table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
@ -204,7 +204,7 @@ class Partition(db.Model):
return group return group
class GroupDescr(db.Model): class GroupDescr(db.Model, ScoDocModel):
"""Description d'un groupe d'une partition""" """Description d'un groupe d'une partition"""
__tablename__ = "group_descr" __tablename__ = "group_descr"

View File

@ -0,0 +1,48 @@
"""évènements scolaires dans la vie d'un étudiant(inscription, ...)
"""
from app import db
from app.models import SHORT_STR_LEN
class ScolarEvent(db.Model):
"""Evenement dans le parcours scolaire d'un étudiant"""
__tablename__ = "scolar_events"
id = db.Column(db.Integer, primary_key=True)
event_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
)
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
# 'ECHEC_SEM'
# 'UTIL_COMPENSATION'
event_type = db.Column(db.String(SHORT_STR_LEN))
# Semestre compensé par formsemestre_id:
comp_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
formsemestre = db.relationship(
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
)
def to_dict(self) -> dict:
"as a dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"

View File

@ -1,6 +1,6 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""Notes, décisions de jury, évènements scolaires """Notes, décisions de jury
""" """
from app import db from app import db
@ -218,47 +218,3 @@ class ScolarAutorisationInscription(db.Model):
msg=f"Passage vers S{autorisation.semestre_id}: effacé", msg=f"Passage vers S{autorisation.semestre_id}: effacé",
) )
db.session.flush() db.session.flush()
class ScolarEvent(db.Model):
"""Evenement dans le parcours scolaire d'un étudiant"""
__tablename__ = "scolar_events"
id = db.Column(db.Integer, primary_key=True)
event_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
)
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
# 'ECHEC_SEM'
# 'UTIL_COMPENSATION'
event_type = db.Column(db.String(SHORT_STR_LEN))
# Semestre compensé par formsemestre_id:
comp_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
formsemestre = db.relationship(
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
)
def to_dict(self) -> dict:
"as a dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"

View File

@ -149,21 +149,49 @@ def index_html(showcodes=0, showsemtable=0):
</p>""" </p>"""
) )
# #
if current_user.has_permission(Permission.EtudInscrit): H.append(
H.append( """<hr>
"""<hr>
<h3>Gestion des étudiants</h3> <h3>Gestion des étudiants</h3>
<ul> <ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a> """
)
if current_user.has_permission(Permission.EtudInscrit):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.etudident_create_form", scodoc_dept=g.scodoc_dept)
}">créer <em>un</em> nouvel étudiant</a>
</li> </li>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a> <li><a class="stdlink" href="{
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans url_for("scolar.form_students_import_excel", scodoc_dept=g.scodoc_dept)
}">importer de nouveaux étudiants</a>
(<em>ne pas utiliser</em> sauf cas particulier&nbsp;: utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre) étudiants importés à un semestre)
</li> </li>
</ul>
""" """
) )
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.export_etudiants_courants", scodoc_dept=g.scodoc_dept)
}">exporter tableau des étudiants des semestres en cours</a>
</li>
"""
)
if current_user.has_permission(
Permission.EtudInscrit
) and sco_preferences.get_preference("portal_url"):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.formsemestre_import_etud_admission",
scodoc_dept=g.scodoc_dept, tous_courants=1)
}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
</li>
"""
)
H.append("</ul>")
# #
if current_user.has_permission(Permission.EditApogee): if current_user.has_permission(Permission.EditApogee):
H.append( H.append(

View File

@ -45,6 +45,12 @@ from app.models.etudiants import (
pivot_year, pivot_year,
) )
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import (
format_civilite,
format_nom,
format_nomprenom,
format_prenom,
)
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app.scodoc import safehtml from app.scodoc import safehtml
@ -102,60 +108,6 @@ def force_uppercase(s):
return s.upper() if s else s return s.upper() if s else s
def format_nomprenom(etud, reverse=False):
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
DEPRECATED: utiliser Identite.nomprenom
"""
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
prenom = format_prenom(etud["prenom"])
civilite = format_civilite(etud["civilite"])
if reverse:
fs = [nom, prenom]
else:
fs = [civilite, prenom, nom]
return " ".join([x for x in fs if x])
def format_prenom(s):
"""Formatte prenom etudiant pour affichage
DEPRECATED: utiliser Identite.prenom_str
"""
if not s:
return ""
frags = s.split()
r = []
for frag in frags:
fs = frag.split("-")
r.append("-".join([x.lower().capitalize() for x in fs]))
return " ".join(r)
def format_nom(s, uppercase=True):
if not s:
return ""
if uppercase:
return s.upper()
else:
return format_prenom(s)
def format_civilite(civilite):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personne ne souhaitant pas d'affichage).
Raises ScoValueError if conversion fails.
"""
try:
return {
"M": "M.",
"F": "Mme",
"X": "",
}[civilite]
except KeyError as exc:
raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc
def _format_etat_civil(etud: dict) -> str: def _format_etat_civil(etud: dict) -> str:
"Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées." "Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées."
if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]: if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]:
@ -657,16 +609,6 @@ def create_etud(cnx, args: dict = None):
db.session.commit() db.session.commit()
etudid = etud.id etudid = etud.id
# event
scolar_events_create(
cnx,
args={
"etudid": etudid,
"event_date": time.strftime("%d/%m/%Y"),
"formsemestre_id": None,
"event_type": "CREATION",
},
)
# log # log
logdb( logdb(
cnx, cnx,
@ -674,16 +616,18 @@ def create_etud(cnx, args: dict = None):
etudid=etudid, etudid=etudid,
msg="creation initiale", msg="creation initiale",
) )
etud = etudident_list(cnx, {"etudid": etudid})[0] etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
fill_etuds_info([etud]) fill_etuds_info([etud_dict])
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) etud_dict["url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud, text=f"Nouvel étudiant {etud.html_link_fiche()}",
url=etud["url"], url=etud_dict["url"],
max_frequency=0, max_frequency=0,
) )
return etud return etud_dict
# ---------- "EVENTS" # ---------- "EVENTS"

View File

@ -40,6 +40,8 @@ from openpyxl.comments import Comment
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.utils import quote_sheetname, absolute_coordinate
from openpyxl.workbook.defined_name import DefinedName
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -218,6 +220,7 @@ class ScoExcelSheet:
self.rows = [] # list of list of cells self.rows = [] # list of list of cells
self.column_dimensions = {} self.column_dimensions = {}
self.row_dimensions = {} self.row_dimensions = {}
self.formulae = {}
def excel_make_composite_style( def excel_make_composite_style(
self, self,
@ -363,6 +366,9 @@ class ScoExcelSheet:
"""ajoute une ligne déjà construite à la feuille.""" """ajoute une ligne déjà construite à la feuille."""
self.rows.append(row) self.rows.append(row)
def set_formula(self, coord, formula):
self.formulae[coord] = formula
def prepare(self): def prepare(self):
"""génére un flux décrivant la feuille. """génére un flux décrivant la feuille.
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille) Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
@ -383,6 +389,8 @@ class ScoExcelSheet:
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
self.prepare() self.prepare()
for coord, formula in self.formulae.items():
self.ws[coord] = formula
with NamedTemporaryFile() as tmp: with NamedTemporaryFile() as tmp:
self.wb.save(tmp.name) self.wb.save(tmp.name)
tmp.seek(0) tmp.seek(0)
@ -433,7 +441,12 @@ def excel_simple_table(
return ws.generate() return ws.generate()
def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines): from openpyxl.utils import absolute_coordinate
def excel_feuille_saisie(
evaluation: "Evaluation", titreannee, description, lines, withnips=False
):
"""Genere feuille excel pour saisie des notes. """Genere feuille excel pour saisie des notes.
E: evaluation (dict) E: evaluation (dict)
lines: liste de tuples lines: liste de tuples
@ -450,6 +463,13 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
ws.set_column_dimension_width("D", 164.0 / 7) # groupes ws.set_column_dimension_width("D", 164.0 / 7) # groupes
ws.set_column_dimension_width("E", 115.0 / 7) # notes ws.set_column_dimension_width("E", 115.0 / 7) # notes
ws.set_column_dimension_width("F", 355.0 / 7) # remarques ws.set_column_dimension_width("F", 355.0 / 7) # remarques
if withnips:
ws.set_column_dimension_width("G", 11.0 / 7) # colonne NIP cachée
ws.set_column_dimension_width(
"H", 105.0 / 7
) # Colonne blanche entre liste et zone de saisie
ws.set_column_dimension_width("I", 90.0 / 7) # Saisie: NIP
ws.set_column_dimension_width("J", 115.0 / 7) # Saisie: Note
# fontes # fontes
font_base = Font(name="Arial", size=12) font_base = Font(name="Arial", size=12)
@ -497,15 +517,32 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
"font": font_blue, "font": font_blue,
"border": border_top, "border": border_top,
} }
style_input = {
"font": font_bold,
"border": border_top,
"fill": fill_light_yellow,
}
if withnips:
del style_notes["fill"]
list_top = 9
# ligne de titres # ligne de titres
ws.append_single_cell_row( ws.append_single_cell_row(
"Feuille saisie note (à enregistrer au format excel)", style_titres "Feuille saisie note (à enregistrer au format excel)", style_titres
) )
# lignes d'instructions # lignes d'instructions
ws.append_single_cell_row( if withnips:
"Saisir les notes dans la colonne E (cases jaunes)", style_expl ws.append_single_cell_row(
) "Saisir les (NIP, note) en colonne I et J (cases jaunes). ordre des NIP indifférent",
style_expl,
)
else:
ws.append_single_cell_row(
"Saisir les notes dans la colonne E (cases jaunes)", style_expl
)
ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl) ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl)
# Nom du semestre # Nom du semestre
ws.append_single_cell_row(scu.unescape_html(titreannee), style_titres) ws.append_single_cell_row(scu.unescape_html(titreannee), style_titres)
@ -518,19 +555,33 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
# ligne blanche # ligne blanche
ws.append_blank_row() ws.append_blank_row()
# code et titres colonnes # code et titres colonnes
ws.append_row( title_row = [
[ ws.make_cell("!%s" % evaluation.id, style_ro),
ws.make_cell("!%s" % evaluation.id, style_ro), ws.make_cell("Nom", style_titres),
ws.make_cell("Nom", style_titres), ws.make_cell("Prénom", style_titres),
ws.make_cell("Prénom", style_titres), ws.make_cell("Groupe", style_titres),
ws.make_cell("Groupe", style_titres), ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres), ws.make_cell("Remarque", style_titres),
ws.make_cell("Remarque", style_titres), ]
if withnips:
title_row += [
ws.make_cell("NIP", style_titres),
ws.make_cell(),
ws.make_cell("NIP", style_titres),
ws.make_cell("Note sur 20", style_titres),
] ]
) ws.append_row(title_row)
# Calcul de la zone de saisie (au format $I$9:$J$45) pour intégration dans la formule
if withnips:
min_row = list_top
max_row = list_top + len(lines) - 1
min_col = "I"
max_col = "J"
input_range = absolute_coordinate(f"{min_col}{min_row}:{max_col}{max_row}")
# etudiants # etudiants
for line in lines: for line_number, line in enumerate(lines):
st = style_nom st = style_nom
if line[3] != "I": if line[3] != "I":
st = style_dem st = style_dem
@ -543,17 +594,30 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
try: try:
val = float(line[5]) val = float(line[5])
except ValueError: except ValueError:
val = line[5] if withnips and line[5] == "":
ws.append_row( ws.set_formula(
[ f"E{list_top + line_number}",
ws.make_cell("!" + line[0], style_ro), # code f"=VLOOKUP(G{list_top + line_number},{input_range}, 2, FALSE)",
ws.make_cell(line[1], st), )
ws.make_cell(line[2], st), val = ""
ws.make_cell(s, st), else:
ws.make_cell(val, style_notes), # note val = line[5]
ws.make_cell(line[6], style_comment), # comment row = [
ws.make_cell("!" + line[0], style_ro), # code
ws.make_cell(line[1], st),
ws.make_cell(line[2], st),
ws.make_cell(s, st),
ws.make_cell(val, style_notes), # note
ws.make_cell(line[6], style_comment), # comment
]
if withnips:
row += [
ws.make_cell(line[7], style_ro), # NIP
ws.make_cell(),
ws.make_cell("", style_input), # Saisie NIP
ws.make_cell("", style_input), # Saisie note
] ]
) ws.append_row(row)
# explication en bas # explication en bas
ws.append_row([None, ws.make_cell("Code notes", style_titres)]) ws.append_row([None, ws.make_cell("Code notes", style_titres)])

View File

@ -42,6 +42,7 @@ from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def form_search_etud( def form_search_etud(
@ -271,7 +272,7 @@ def search_etud_by_name(term: str) -> list:
data = [ data = [
{ {
"label": "%s %s %s" "label": "%s %s %s"
% (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])), % (x["code_nip"], x["nom"], scu.format_prenom(x["prenom"])),
"value": x["code_nip"], "value": x["code_nip"],
} }
for x in r for x in r
@ -290,7 +291,7 @@ def search_etud_by_name(term: str) -> list:
data = [ data = [
{ {
"label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])), "label": "%s %s" % (x["nom"], scu.format_prenom(x["prenom"])),
"value": x["etudid"], "value": x["etudid"],
} }
for x in r for x in r

View File

@ -39,7 +39,7 @@ from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.groups import Partition, GroupDescr from app.models.groups import Partition, GroupDescr
from app.models.validations import ScolarEvent from app.models.scolar_event import ScolarEvent
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
@ -222,10 +222,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""SELECT Im.id AS moduleimpl_inscription_id """SELECT Im.id AS moduleimpl_inscription_id
FROM notes_moduleimpl_inscription Im, notes_moduleimpl M FROM notes_moduleimpl_inscription Im, notes_moduleimpl M
WHERE Im.etudid=%(etudid)s WHERE Im.etudid=%(etudid)s
and Im.moduleimpl_id = M.id and Im.moduleimpl_id = M.id
and M.formsemestre_id = %(formsemestre_id)s and M.formsemestre_id = %(formsemestre_id)s
""", """,
{"etudid": etudid, "formsemestre_id": formsemestre_id}, {"etudid": etudid, "formsemestre_id": formsemestre_id},
@ -253,7 +253,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
nbinscrits = len(inscrits) nbinscrits = len(inscrits)
if nbinscrits == 0: if nbinscrits == 0:
log( log(
f"""do_formsemestre_desinscription: f"""do_formsemestre_desinscription:
suppression du semestre extérieur {formsemestre}""" suppression du semestre extérieur {formsemestre}"""
) )
flash("Semestre exterieur supprimé") flash("Semestre exterieur supprimé")
@ -436,7 +436,7 @@ def formsemestre_inscription_with_modules(
if inscr is not None: if inscr is not None:
H.append( H.append(
f""" f"""
<p class="warning">{etud.nomprenom} est déjà inscrit <p class="warning">{etud.nomprenom} est déjà inscrit
dans le semestre {formsemestre.titre_mois()} dans le semestre {formsemestre.titre_mois()}
</p> </p>
<ul> <ul>
@ -482,8 +482,8 @@ def formsemestre_inscription_with_modules(
H.append("</ul>") H.append("</ul>")
H.append( H.append(
f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules", f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id, scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
multiple_ok=1, multiple_ok=1,
group_ids=group_ids ) group_ids=group_ids )
}">Continuer quand même l'inscription</a> }">Continuer quand même l'inscription</a>
</p>""" </p>"""
@ -644,7 +644,7 @@ function chkbx_select(field_id, state) {
""" """
<p>Voici la liste des modules du semestre choisi.</p> <p>Voici la liste des modules du semestre choisi.</p>
<p> <p>
Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. Les modules cochés sont ceux dans lesquels l'étudiant est inscrit.
Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules. Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.
</p> </p>
<p>Attention: cette méthode ne devrait être utilisée que pour les modules <p>Attention: cette méthode ne devrait être utilisée que pour les modules

View File

@ -53,6 +53,7 @@ from app.scodoc import codes_cursus
from app.scodoc import sco_cursus from app.scodoc import sco_cursus
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_etud import etud_sort_key from app.scodoc.sco_etud import etud_sort_key
import app.scodoc.sco_utils as scu
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
@ -573,8 +574,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
etudid=str(e["etudid"]), etudid=str(e["etudid"]),
civilite=etud["civilite_str"] or "", civilite=etud["civilite_str"] or "",
sexe=etud["civilite_str"] or "", # compat sexe=etud["civilite_str"] or "", # compat
nom=sco_etud.format_nom(etud["nom"] or ""), nom=scu.format_nom(etud["nom"] or ""),
prenom=sco_etud.format_prenom(etud["prenom"] or ""), prenom=scu.format_prenom(etud["prenom"] or ""),
origin=_comp_etud_origin(etud, formsemestre), origin=_comp_etud_origin(etud, formsemestre),
) )
) )
@ -599,8 +600,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
"etud", "etud",
etudid=str(etud["etudid"]), etudid=str(etud["etudid"]),
sexe=etud["civilite_str"] or "", sexe=etud["civilite_str"] or "",
nom=sco_etud.format_nom(etud["nom"] or ""), nom=scu.format_nom(etud["nom"] or ""),
prenom=sco_etud.format_prenom(etud["prenom"] or ""), prenom=scu.format_prenom(etud["prenom"] or ""),
origin=_comp_etud_origin(etud, formsemestre), origin=_comp_etud_origin(etud, formsemestre),
) )
) )

View File

@ -101,7 +101,8 @@ def group_rename(group_id):
"allow_null": True, "allow_null": True,
"explanation": """optionnel : identifiant du groupe dans le logiciel "explanation": """optionnel : identifiant du groupe dans le logiciel
d'emploi du temps, pour le cas où les noms de groupes ne seraient pas d'emploi du temps, pour le cas où les noms de groupes ne seraient pas
les mêmes dans ScoDoc et dans l'emploi du temps (si plusieurs ids, les mêmes dans ScoDoc et dans l'emploi du temps (si plusieurs ids de
groupes EDT doivent correspondre au même groupe ScoDoc,
les séparer par des virgules).""", les séparer par des virgules).""",
}, },
), ),

View File

@ -254,7 +254,7 @@ def import_users(users, force="") -> tuple[bool, list[str], int]:
if import_ok: if import_ok:
for u in created.values(): for u in created.values():
# Création de l'utilisateur (via SQLAlchemy) # Création de l'utilisateur (via SQLAlchemy)
user = User() user = User(user_name=u["user_name"])
user.from_dict(u, new_user=True) user.from_dict(u, new_user=True)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()

View File

@ -244,8 +244,8 @@ def feuille_preparation_jury(formsemestre_id):
[ [
etud.id, etud.id,
etud.civilite_str, etud.civilite_str,
sco_etud.format_nom(etud.nom), scu.format_nom(etud.nom),
sco_etud.format_prenom(etud.prenom), scu.format_prenom(etud.prenom),
etud.date_naissance, etud.date_naissance,
etud.admission.bac if etud.admission else "", etud.admission.bac if etud.admission else "",
etud.admission.specialite if etud.admission else "", etud.admission.specialite if etud.admission else "",

View File

@ -733,6 +733,8 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
<li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{ <li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
groups_infos.groups_query_args}" groups_infos.groups_query_args}"
id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a> id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a>
&nbsp;(<a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
groups_infos.groups_query_args}&withnips=1">saisie par NIP</a>)
</li> </li>
<li>ou <a class="stdlink" href="{url_for("notes.saisie_notes", <li>ou <a class="stdlink" href="{url_for("notes.saisie_notes",
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id) scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
@ -875,7 +877,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
return "\n".join(H) return "\n".join(H)
def feuille_saisie_notes(evaluation_id, group_ids=[]): def feuille_saisie_notes(evaluation_id, group_ids=[], withnips=0):
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués""" """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id) evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
if not evaluation: if not evaluation:
@ -922,28 +924,31 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
# une liste de liste de chaines: lignes de la feuille de calcul # une liste de liste de chaines: lignes de la feuille de calcul
rows = [] rows = []
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id) etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id)
for e in etuds: for e in etuds:
etudid = e["etudid"] etudid = e["etudid"]
groups = sco_groups.get_etud_groups(etudid, formsemestre.id) groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
grc = sco_groups.listgroups_abbrev(groups) grc = sco_groups.listgroups_abbrev(groups)
row = [
rows.append( str(etudid),
[ e["nom"].upper(),
str(etudid), e["prenom"].lower().capitalize(),
e["nom"].upper(), e["inscr"]["etat"],
e["prenom"].lower().capitalize(), grc,
e["inscr"]["etat"], e["val"],
grc, e["explanation"],
e["val"], ]
e["explanation"], if withnips == 1:
] row.append(e["code_nip"])
) rows.append(row)
filename = f"notes_{eval_name}_{gr_title_filename}" filename = f"notes_{eval_name}_{gr_title_filename}"
xls = sco_excel.excel_feuille_saisie( xls = sco_excel.excel_feuille_saisie(
evaluation, formsemestre.titre_annee(), description, lines=rows evaluation,
formsemestre.titre_annee(),
description,
lines=rows,
withnips=(withnips == 1),
) )
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
@ -1244,7 +1249,7 @@ def _form_saisie_notes(
'<span class="%s">' % classdem '<span class="%s">' % classdem
+ e["civilite_str"] + e["civilite_str"]
+ " " + " "
+ sco_etud.format_nomprenom(e, reverse=True) + scu.format_nomprenom(e, reverse=True)
+ "</span>" + "</span>"
) )

View File

@ -793,21 +793,25 @@ def update_etape_formsemestre_inscription(ins, etud):
def formsemestre_import_etud_admission( def formsemestre_import_etud_admission(
formsemestre_id, import_identite=True, import_email=False formsemestre_id: int, import_identite=True, import_email=False
): ) -> tuple[list[Identite], list[Identite], list[tuple[Identite, str]]]:
"""Tente d'importer les données admission depuis le portail """Tente d'importer les données admission depuis le portail
pour tous les étudiants du semestre. pour tous les étudiants du semestre.
Si import_identite==True, recopie l'identité (nom/prenom/sexe/date_naissance) Si import_identite==True, recopie l'identité (nom/prenom/sexe/date_naissance)
de chaque étudiant depuis le portail. de chaque étudiant depuis le portail.
N'affecte pas les etudiants inconnus sur le portail. N'affecte pas les etudiants inconnus sur le portail.
Renvoie:
- etuds_no_nip: liste d'étudiants sans code NIP
- etuds_unknown: etudiants avec NIP mais inconnus du portail
- changed_mails: (etudiant, old_mail) pour ceux dont le mail a changé
""" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
{"formsemestre_id": formsemestre_id} {"formsemestre_id": formsemestre_id}
) )
log(f"formsemestre_import_etud_admission: {formsemestre_id} ({len(ins)} etuds)") log(f"formsemestre_import_etud_admission: {formsemestre_id} ({len(ins)} etuds)")
no_nip = [] # liste d'etudids sans code NIP etuds_no_nip: list[Identite] = []
unknowns = [] # etudiants avec NIP mais inconnus du portail etuds_unknown: list[Identite] = []
changed_mails: list[tuple[Identite, str]] = [] # modification d'adresse mails changed_mails: list[tuple[Identite, str]] = [] # modification d'adresse mails
# Essaie de recuperer les etudiants des étapes, car # Essaie de recuperer les etudiants des étapes, car
@ -828,7 +832,7 @@ def formsemestre_import_etud_admission(
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
code_nip = etud.code_nip code_nip = etud.code_nip
if not code_nip: if not code_nip:
no_nip.append(etudid) etuds_no_nip.append(etud)
else: else:
data_apo = apo_etuds.get(code_nip) data_apo = apo_etuds.get(code_nip)
if not data_apo: if not data_apo:
@ -865,7 +869,7 @@ def formsemestre_import_etud_admission(
if adresse.email != data_apo["mail"]: if adresse.email != data_apo["mail"]:
changed_mails.append((etud, old_mail)) changed_mails.append((etud, old_mail))
else: else:
unknowns.append(code_nip) etuds_unknown.append(etud)
db.session.commit() db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"]) sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])
return no_nip, unknowns, changed_mails return etuds_no_nip, etuds_unknown, changed_mails

View File

@ -129,7 +129,7 @@ def trombino_html(groups_infos):
H = [ H = [
f"""<table style="padding-top: 10px; padding-bottom: 10px;"> f"""<table style="padding-top: 10px; padding-bottom: 10px;">
<tr> <tr>
<td><span <td><span
style="font-style: bold; font-size: 150%%; padding-right: 20px;" style="font-style: bold; font-size: 150%%; padding-right: 20px;"
>{group_txt}</span></td>""" >{group_txt}</span></td>"""
] ]
@ -164,9 +164,9 @@ def trombino_html(groups_infos):
H.append("</span>") H.append("</span>")
H.append( H.append(
'<span class="trombi_legend"><span class="trombi_prenom">' '<span class="trombi_legend"><span class="trombi_prenom">'
+ sco_etud.format_prenom(t["prenom"]) + scu.format_prenom(t["prenom"])
+ '</span><span class="trombi_nom">' + '</span><span class="trombi_nom">'
+ sco_etud.format_nom(t["nom"]) + scu.format_nom(t["nom"])
+ (" <i>(dem.)</i>" if t["etat"] == "D" else "") + (" <i>(dem.)</i>" if t["etat"] == "D" else "")
) )
H.append("</span></span></span>") H.append("</span></span></span>")
@ -175,10 +175,10 @@ def trombino_html(groups_infos):
H.append("</div>") H.append("</div>")
H.append( H.append(
f"""<div style="margin-bottom:15px;"> f"""<div style="margin-bottom:15px;">
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a> fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a> fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
</div>""" </div>"""
) )
@ -202,9 +202,9 @@ def check_local_photos_availability(groups_infos, fmt=""):
return ( return (
False, False,
scu.confirm_dialog( scu.confirm_dialog(
f"""<p>Attention: {nb_missing} photos ne sont pas disponibles f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
et ne peuvent pas être exportées.</p> et ne peuvent pas être exportées.</p>
<p>Vous pouvez <a class="stdlink" <p>Vous pouvez <a class="stdlink"
href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}" href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}"
>exporter seulement les photos existantes</a>""", >exporter seulement les photos existantes</a>""",
dest_url="trombino", dest_url="trombino",
@ -263,11 +263,11 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
f"""<h2>Copier les photos du portail vers ScoDoc ?</h2> f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
<p>Les photos du groupe {groups_infos.groups_titles} présentes <p>Les photos du groupe {groups_infos.groups_titles} présentes
dans ScoDoc seront remplacées par celles du portail (si elles existent). dans ScoDoc seront remplacées par celles du portail (si elles existent).
</p> </p>
<p>(les photos sont normalement automatiquement copiées <p>(les photos sont normalement automatiquement copiées
lors de leur première utilisation, l'usage de cette fonction lors de leur première utilisation, l'usage de cette fonction
n'est nécessaire que si les photos du portail ont été modifiées) n'est nécessaire que si les photos du portail ont été modifiées)
</p> </p>
""", """,
@ -349,7 +349,7 @@ def _trombino_pdf(groups_infos):
[img], [img],
[ [
Paragraph( Paragraph(
SU(sco_etud.format_nomprenom(t)), SU(scu.format_nomprenom(t)),
style_sheet["Normal"], style_sheet["Normal"],
) )
], ],
@ -428,7 +428,7 @@ def _listeappel_photos_pdf(groups_infos):
t = groups_infos.members[i] t = groups_infos.members[i]
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH) img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
txt = Paragraph( txt = Paragraph(
SU(sco_etud.format_nomprenom(t)), SU(scu.format_nomprenom(t)),
style_sheet["Normal"], style_sheet["Normal"],
) )
if currow: if currow:

View File

@ -55,7 +55,7 @@ def trombino_doc(groups_infos):
cell = table.rows[2 * li + 1].cells[co] cell = table.rows[2 * li + 1].cells[co]
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
cell_p, cell_f, cell_r = _paragraph_format_run(cell) cell_p, cell_f, cell_r = _paragraph_format_run(cell)
cell_r.add_text(sco_etud.format_nomprenom(t)) cell_r.add_text(scu.format_nomprenom(t))
cell_f.space_after = Mm(8) cell_f.space_after = Mm(8)
return scu.send_docx(document, filename) return scu.send_docx(document, filename)

View File

@ -196,9 +196,9 @@ def pdf_trombino_tours(
Paragraph( Paragraph(
SU( SU(
"<para align=center><font size=8>" "<para align=center><font size=8>"
+ sco_etud.format_prenom(m["prenom"]) + scu.format_prenom(m["prenom"])
+ " " + " "
+ sco_etud.format_nom(m["nom"]) + scu.format_nom(m["nom"])
+ text_group + text_group
+ "</font></para>" + "</font></para>"
), ),
@ -413,11 +413,7 @@ def pdf_feuille_releve_absences(
for m in members: for m in members:
currow = [ currow = [
Paragraph( Paragraph(
SU( SU(scu.format_nom(m["nom"]) + " " + scu.format_prenom(m["prenom"])),
sco_etud.format_nom(m["nom"])
+ " "
+ sco_etud.format_prenom(m["prenom"])
),
StyleSheet["Normal"], StyleSheet["Normal"],
) )
] ]

View File

@ -102,7 +102,7 @@ def index_html(
<option value="">--Choisir--</option> <option value="">--Choisir--</option>
{menu_roles} {menu_roles}
</select> </select>
</form> </form>
""" """
) )
@ -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",
@ -431,15 +432,3 @@ def check_modif_user(
) )
# Roles ? # Roles ?
return True, "" return True, ""
def user_edit(user_name, vals):
"""Edit the user specified by user_name
(ported from Zope to SQLAlchemy, hence strange !)
"""
u: User = User.query.filter_by(user_name=user_name).first()
if not u:
raise ScoValueError("Invalid user_name")
u.from_dict(vals)
db.session.add(u)
db.session.commit()

View File

@ -64,6 +64,7 @@ from config import Config
from app import log, ScoDocJSONEncoder from app import log, ScoDocJSONEncoder
from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_xml from app.scodoc import sco_xml
import sco_version import sco_version
@ -204,6 +205,13 @@ class EtatAssiduite(int, BiDirectionalEnum):
RETARD = 1 RETARD = 1
ABSENT = 2 ABSENT = 2
def version_lisible(self) -> str:
return {
EtatAssiduite.PRESENT: "Présence",
EtatAssiduite.ABSENT: "Absence",
EtatAssiduite.RETARD: "Retard",
}.get(self, "")
class EtatJustificatif(int, BiDirectionalEnum): class EtatJustificatif(int, BiDirectionalEnum):
"""Code des états des justificatifs""" """Code des états des justificatifs"""
@ -215,6 +223,14 @@ class EtatJustificatif(int, BiDirectionalEnum):
ATTENTE = 2 ATTENTE = 2
MODIFIE = 3 MODIFIE = 3
def version_lisible(self) -> str:
return {
EtatJustificatif.VALIDE: "valide",
EtatJustificatif.ATTENTE: "soumis",
EtatJustificatif.MODIFIE: "modifié",
EtatJustificatif.NON_VALIDE: "invalide",
}.get(self, "")
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None: def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
""" """
@ -1139,6 +1155,61 @@ def abbrev_prenom(prenom):
return abrv return abrv
def format_civilite(civilite):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personne ne souhaitant pas d'affichage).
Raises ScoValueError if conversion fails.
"""
try:
return {
"M": "M.",
"F": "Mme",
"X": "",
}[civilite]
except KeyError as exc:
raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc
def format_nomprenom(etud, reverse=False):
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
DEPRECATED: utiliser Identite.nomprenom
"""
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
prenom = format_prenom(etud["prenom"])
civilite = format_civilite(etud["civilite"])
if reverse:
fs = [nom, prenom]
else:
fs = [civilite, prenom, nom]
return " ".join([x for x in fs if x])
def format_nom(s, uppercase=True):
"Formatte le nom"
if not s:
return ""
if uppercase:
return s.upper()
else:
return format_prenom(s)
def format_prenom(s):
"""Formatte prenom etudiant pour affichage
DEPRECATED: utiliser Identite.prenom_str
"""
if not s:
return ""
frags = s.split()
r = []
for frag in frags:
fs = frag.split("-")
r.append("-".join([x.lower().capitalize() for x in fs]))
return " ".join(r)
# #
def timedate_human_repr(): def timedate_human_repr():
"representation du temps courant pour utilisateur" "representation du temps courant pour utilisateur"
@ -1480,6 +1551,7 @@ def is_assiduites_module_forced(
def get_assiduites_time_config(config_type: str) -> str: def get_assiduites_time_config(config_type: str) -> str:
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
match config_type: match config_type:
case "matin": case "matin":
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00") return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")

View File

@ -1,4 +1,4 @@
/* /*
* DataTables style for ScoDoc gen_tables * DataTables style for ScoDoc gen_tables
* generated using https://datatables.net/manual/styling/theme-creator * generated using https://datatables.net/manual/styling/theme-creator
* and customized by hand * and customized by hand
@ -138,111 +138,111 @@ table.dataTable.display tbody tr:hover.selected {
background-color: #a9b7d1; background-color: #a9b7d1;
} }
table.dataTable.order-column tbody tr>.sorting_1, table.dataTable.order-column tbody tr > .sorting_1,
table.dataTable.order-column tbody tr>.sorting_2, table.dataTable.order-column tbody tr > .sorting_2,
table.dataTable.order-column tbody tr>.sorting_3, table.dataTable.order-column tbody tr > .sorting_3,
table.dataTable.display tbody tr>.sorting_1, table.dataTable.display tbody tr > .sorting_1,
table.dataTable.display tbody tr>.sorting_2, table.dataTable.display tbody tr > .sorting_2,
table.dataTable.display tbody tr>.sorting_3 { table.dataTable.display tbody tr > .sorting_3 {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
table.dataTable.order-column tbody tr.selected>.sorting_1, table.dataTable.order-column tbody tr.selected > .sorting_1,
table.dataTable.order-column tbody tr.selected>.sorting_2, table.dataTable.order-column tbody tr.selected > .sorting_2,
table.dataTable.order-column tbody tr.selected>.sorting_3, table.dataTable.order-column tbody tr.selected > .sorting_3,
table.dataTable.display tbody tr.selected>.sorting_1, table.dataTable.display tbody tr.selected > .sorting_1,
table.dataTable.display tbody tr.selected>.sorting_2, table.dataTable.display tbody tr.selected > .sorting_2,
table.dataTable.display tbody tr.selected>.sorting_3 { table.dataTable.display tbody tr.selected > .sorting_3 {
background-color: #acbad4; background-color: #acbad4;
} }
table.dataTable.display tbody tr.odd>.sorting_1, table.dataTable.display tbody tr.odd > .sorting_1,
table.dataTable.order-column.stripe tbody tr.odd>.sorting_1 { table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
background-color: #f1f1f1; background-color: #f1f1f1;
} }
table.dataTable.display tbody tr.odd>.sorting_2, table.dataTable.display tbody tr.odd > .sorting_2,
table.dataTable.order-column.stripe tbody tr.odd>.sorting_2 { table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
background-color: #f3f3f3; background-color: #f3f3f3;
} }
table.dataTable.display tbody tr.odd>.sorting_3, table.dataTable.display tbody tr.odd > .sorting_3,
table.dataTable.order-column.stripe tbody tr.odd>.sorting_3 { table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
background-color: whitesmoke; background-color: whitesmoke;
} }
table.dataTable.display tbody tr.odd.selected>.sorting_1, table.dataTable.display tbody tr.odd.selected > .sorting_1,
table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1 { table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
background-color: #a6b3cd; background-color: #a6b3cd;
} }
table.dataTable.display tbody tr.odd.selected>.sorting_2, table.dataTable.display tbody tr.odd.selected > .sorting_2,
table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2 { table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
background-color: #a7b5ce; background-color: #a7b5ce;
} }
table.dataTable.display tbody tr.odd.selected>.sorting_3, table.dataTable.display tbody tr.odd.selected > .sorting_3,
table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3 { table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
background-color: #a9b6d0; background-color: #a9b6d0;
} }
table.dataTable.display tbody tr.even>.sorting_1, table.dataTable.display tbody tr.even > .sorting_1,
table.dataTable.order-column.stripe tbody tr.even>.sorting_1 { table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
table.dataTable.display tbody tr.even>.sorting_2, table.dataTable.display tbody tr.even > .sorting_2,
table.dataTable.order-column.stripe tbody tr.even>.sorting_2 { table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
background-color: #fbfbfb; background-color: #fbfbfb;
} }
table.dataTable.display tbody tr.even>.sorting_3, table.dataTable.display tbody tr.even > .sorting_3,
table.dataTable.order-column.stripe tbody tr.even>.sorting_3 { table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
background-color: #fdfdfd; background-color: #fdfdfd;
} }
table.dataTable.display tbody tr.even.selected>.sorting_1, table.dataTable.display tbody tr.even.selected > .sorting_1,
table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1 { table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
background-color: #acbad4; background-color: #acbad4;
} }
table.dataTable.display tbody tr.even.selected>.sorting_2, table.dataTable.display tbody tr.even.selected > .sorting_2,
table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2 { table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
background-color: #adbbd6; background-color: #adbbd6;
} }
table.dataTable.display tbody tr.even.selected>.sorting_3, table.dataTable.display tbody tr.even.selected > .sorting_3,
table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3 { table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
background-color: #afbdd8; background-color: #afbdd8;
} }
table.dataTable.display tbody tr:hover>.sorting_1, table.dataTable.display tbody tr:hover > .sorting_1,
table.dataTable.order-column.hover tbody tr:hover>.sorting_1 { table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
background-color: #eaeaea; background-color: #eaeaea;
} }
table.dataTable.display tbody tr:hover>.sorting_2, table.dataTable.display tbody tr:hover > .sorting_2,
table.dataTable.order-column.hover tbody tr:hover>.sorting_2 { table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
background-color: #ebebeb; background-color: #ebebeb;
} }
table.dataTable.display tbody tr:hover>.sorting_3, table.dataTable.display tbody tr:hover > .sorting_3,
table.dataTable.order-column.hover tbody tr:hover>.sorting_3 { table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
background-color: #eeeeee; background-color: #eeeeee;
} }
table.dataTable.display tbody tr:hover.selected>.sorting_1, table.dataTable.display tbody tr:hover.selected > .sorting_1,
table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1 { table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
background-color: #a1aec7; background-color: #a1aec7;
} }
table.dataTable.display tbody tr:hover.selected>.sorting_2, table.dataTable.display tbody tr:hover.selected > .sorting_2,
table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2 { table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
background-color: #a2afc8; background-color: #a2afc8;
} }
table.dataTable.display tbody tr:hover.selected>.sorting_3, table.dataTable.display tbody tr:hover.selected > .sorting_3,
table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3 { table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
background-color: #a4b2cb; background-color: #a4b2cb;
} }
@ -419,7 +419,13 @@ table.dataTable td {
color: #333333 !important; color: #333333 !important;
border: 1px solid #979797; border: 1px solid #979797;
background-color: white; background-color: white;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, gainsboro)); background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, white),
color-stop(100%, gainsboro)
);
/* Chrome,Safari4+ */ /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, white 0%, gainsboro 100%); background: -webkit-linear-gradient(top, white 0%, gainsboro 100%);
/* Chrome10+,Safari5.1+ */ /* Chrome10+,Safari5.1+ */
@ -447,7 +453,13 @@ table.dataTable td {
color: white !important; color: white !important;
border: 1px solid #111111; border: 1px solid #111111;
background-color: #585858; background-color: #585858;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111111)); background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, #585858),
color-stop(100%, #111111)
);
/* Chrome,Safari4+ */ /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #585858 0%, #111111 100%); background: -webkit-linear-gradient(top, #585858 0%, #111111 100%);
/* Chrome10+,Safari5.1+ */ /* Chrome10+,Safari5.1+ */
@ -464,7 +476,13 @@ table.dataTable td {
.dataTables_wrapper .dataTables_paginate .paginate_button:active { .dataTables_wrapper .dataTables_paginate .paginate_button:active {
outline: none; outline: none;
background-color: #2b2b2b; background-color: #2b2b2b;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c)); background: -webkit-gradient(
linear,
left top,
left bottom,
color-stop(0%, #2b2b2b),
color-stop(100%, #0c0c0c)
);
/* Chrome,Safari4+ */ /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
/* Chrome10+,Safari5.1+ */ /* Chrome10+,Safari5.1+ */
@ -495,12 +513,50 @@ table.dataTable td {
text-align: center; text-align: center;
font-size: 1.2em; font-size: 1.2em;
background-color: white; background-color: white;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0))); background: -webkit-gradient(
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); linear,
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); left top,
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); right top,
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); color-stop(0%, rgba(255, 255, 255, 0)),
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); color-stop(25%, rgba(255, 255, 255, 0.9)),
color-stop(75%, rgba(255, 255, 255, 0.9)),
color-stop(100%, rgba(255, 255, 255, 0))
);
background: -webkit-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -moz-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -ms-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: -o-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.9) 25%,
rgba(255, 255, 255, 0.9) 75%,
rgba(255, 255, 255, 0) 100%
);
} }
.dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_length,
@ -520,17 +576,69 @@ table.dataTable td {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th, .dataTables_wrapper
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td, .dataTables_scroll
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th, div.dataTables_scrollBody
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td { > table
> thead
> tr
> th,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> td,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> th,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> td {
vertical-align: middle; vertical-align: middle;
} }
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing, .dataTables_wrapper
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing, .dataTables_scroll
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing, div.dataTables_scrollBody
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing { > table
> thead
> tr
> th
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> thead
> tr
> td
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> th
> div.dataTables_sizing,
.dataTables_wrapper
.dataTables_scroll
div.dataTables_scrollBody
> table
> tbody
> tr
> td
> div.dataTables_sizing {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
margin: 0 !important; margin: 0 !important;
@ -541,8 +649,8 @@ table.dataTable td {
border-bottom: 1px solid #111111; border-bottom: 1px solid #111111;
} }
.dataTables_wrapper.no-footer div.dataTables_scrollHead>table, .dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
.dataTables_wrapper.no-footer div.dataTables_scrollBody>table { .dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
border-bottom: none; border-bottom: none;
} }
@ -555,7 +663,6 @@ table.dataTable td {
} }
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
.dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_paginate { .dataTables_wrapper .dataTables_paginate {
float: none; float: none;
@ -568,7 +675,6 @@ table.dataTable td {
} }
@media screen and (max-width: 640px) { @media screen and (max-width: 640px) {
.dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter { .dataTables_wrapper .dataTables_filter {
float: none; float: none;
@ -580,8 +686,7 @@ table.dataTable td {
} }
} }
/* ------ Ajouts spécifiques pour ScoDoc:
/* ------ Ajouts spécifiques pour ScoDoc:
*/ */
table.gt_table td { table.gt_table td {
@ -624,7 +729,6 @@ table.dataTable.stripe.hover tbody tr.odd:hover td,
table.dataTable.stripe.hover tbody tr.odd:hover td.sorting_1, table.dataTable.stripe.hover tbody tr.odd:hover td.sorting_1,
table.dataTable.order-column.stripe.hover tbody tr.odd:hover td.sorting_1 { table.dataTable.order-column.stripe.hover tbody tr.odd:hover td.sorting_1 {
background-color: rgb(80%, 85%, 80%); background-color: rgb(80%, 85%, 80%);
;
} }
/* Lignes paires */ /* Lignes paires */
@ -638,7 +742,6 @@ table.dataTable.stripe.hover tbody tr.even:hover td,
table.dataTable.stripe.hover tbody tr.even:hover td.sorting_1, table.dataTable.stripe.hover tbody tr.even:hover td.sorting_1,
table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 { table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
background-color: rgb(85%, 85%, 85%); background-color: rgb(85%, 85%, 85%);
;
} }
/* Reglage largeur de la table */ /* Reglage largeur de la table */
@ -652,4 +755,9 @@ table.dataTable.gt_table {
/* Tables non centrées (inutile) */ /* Tables non centrées (inutile) */
table.dataTable.gt_table.gt_left { table.dataTable.gt_table.gt_left {
margin-left: 16px; margin-left: 16px;
} }
table.dataTable.gt_table.gt_left td,
table.dataTable.gt_table.gt_left th {
text-align: left;
}
scodoc;css

View File

@ -1133,7 +1133,8 @@ a.redlink:hover {
text-decoration: underline; text-decoration: underline;
} }
a.discretelink { a.discretelink,
a:discretelink:visited {
color: black; color: black;
text-decoration: none; text-decoration: none;
} }
@ -1567,6 +1568,7 @@ h2.formsemestre,
#gtrcontent h2 { #gtrcontent h2 {
margin-top: 2px; margin-top: 2px;
font-size: 130%; font-size: 130%;
font-weight: bold;
} }
.formsemestre_page_title table.semtitle, .formsemestre_page_title table.semtitle,
@ -4347,6 +4349,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 +4862,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);
}

View File

@ -448,6 +448,13 @@ class ScoDocDateTimePicker extends HTMLElement {
// Ajouter le style au shadow DOM // Ajouter le style au shadow DOM
shadow.appendChild(style); shadow.appendChild(style);
//Si une value est donnée
let value = this.getAttribute("value");
if (value != null) {
this.value = value;
}
} }
static get observedAttributes() { static get observedAttributes() {
@ -474,7 +481,7 @@ class ScoDocDateTimePicker extends HTMLElement {
} else { } else {
// Mettre à jour la valeur de l'input caché avant la soumission // Mettre à jour la valeur de l'input caché avant la soumission
this.hiddenInput.value = this.isValid() this.hiddenInput.value = this.isValid()
? this.valueAsDate.toIsoUtcString() ? this.valueAsDate.toFakeIso()
: ""; : "";
} }
}); });

View File

@ -6,9 +6,10 @@
"""Liste simple d'étudiants """Liste simple d'étudiants
""" """
import datetime
from flask import g, url_for from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import Identite from app.scodoc.sco_exceptions import ScoValueError
from app.tables import table_builder as tb from app.tables import table_builder as tb
@ -26,6 +27,7 @@ class TableEtud(tb.Table):
with_foot_titles=False, with_foot_titles=False,
**kwargs, **kwargs,
): ):
etuds = etuds or []
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
classes = classes or ["gt_table", "gt_left"] classes = classes or ["gt_table", "gt_left"]
super().__init__( super().__init__(
@ -46,10 +48,12 @@ class TableEtud(tb.Table):
class RowEtud(tb.Row): class RowEtud(tb.Row):
"Ligne de la table d'étudiants" "Ligne de la table d'étudiants"
# pour le moment très simple, extensible (codes, liens bulletins, ...) # pour le moment très simple, extensible (codes, liens bulletins, ...)
def __init__(self, table: TableEtud, etud: Identite, *args, **kwargs): def __init__(self, table: TableEtud, etud: Identite, *args, **kwargs):
super().__init__(table, etud.id, *args, **kwargs) super().__init__(table, etud.id, *args, **kwargs)
self.etud = etud self.etud = etud
self.target_url = etud.url_fiche()
def add_etud_cols(self): def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms""" """Ajoute colonnes étudiant: codes, noms"""
@ -77,7 +81,6 @@ class RowEtud(tb.Row):
# formsemestre_id=res.formsemestre.id, # formsemestre_id=res.formsemestre.id,
# etudid=etud.id, # etudid=etud.id,
# ) # )
url_bulletin = None # pour extension future
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
self.add_cell( self.add_cell(
"nom_disp", "nom_disp",
@ -85,23 +88,10 @@ class RowEtud(tb.Row):
etud.nom_disp(), etud.nom_disp(),
"identite_detail", "identite_detail",
data={"order": etud.sort_key}, data={"order": etud.sort_key},
target=url_bulletin, target=self.target_url,
target_attrs={"class": "etudinfo discretelink", "id": str(etud.id)}, target_attrs={"class": "etudinfo discretelink", "id": str(etud.id)},
) )
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
# self.add_cell(
# "nom_short",
# "Nom",
# etud.nom_short,
# "identite_court",
# data={
# "order": etud.sort_key,
# "etudid": etud.id,
# "nomprenom": etud.nomprenom,
# },
# target=url_bulletin,
# target_attrs={"class": "etudinfo", "id": str(etud.id)},
# )
def etuds_sorted_from_ids(etudids) -> list[Identite]: def etuds_sorted_from_ids(etudids) -> list[Identite]:
@ -115,3 +105,73 @@ def html_table_etuds(etudids) -> str:
etuds = etuds_sorted_from_ids(etudids) etuds = etuds_sorted_from_ids(etudids)
table = TableEtud(etuds) table = TableEtud(etuds)
return table.html() return table.html()
class RowEtudWithInfos(RowEtud):
"""Ligne de la table d'étudiants avec plus d'informations:
département, formsemestre, codes, boursier
"""
def __init__(
self,
table: TableEtud,
etud: Identite,
formsemestre: FormSemestre,
inscription: FormSemestreInscription,
*args,
**kwargs,
):
super().__init__(table, etud, *args, **kwargs)
self.formsemestre = formsemestre
self.inscription = inscription
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
self.add_cell("dept", "Dépt.", self.etud.departement.acronym, "identite_detail")
self.add_cell(
"formsemestre",
"Semestre",
f"""{self.formsemestre.titre_formation()} {
('S'+str(self.formsemestre.semestre_id))
if self.formsemestre.semestre_id > 0 else ''}
""",
"identite_detail",
)
self.add_cell("code_nip", "NIP", self.etud.code_nip or "", "identite_detail")
super().add_etud_cols()
self.add_cell(
"etat",
"État",
self.inscription.etat,
"inscription",
)
self.add_cell(
"boursier",
"Boursier",
"O" if self.etud.boursier else "N",
"identite_infos",
)
class TableEtudWithInfos(TableEtud):
"""Table d'étudiants avec formsemestre et inscription"""
def add_formsemestre(self, formsemestre: FormSemestre):
"Ajoute les étudiants de ce semestre à la table"
etuds = formsemestre.get_inscrits(order=True, include_demdef=True)
for etud in etuds:
row = self.row_class(
self, etud, formsemestre, formsemestre.etuds_inscriptions.get(etud.id)
)
row.add_etud_cols()
self.add_row(row)
def table_etudiants_courants(formsemestres: list[FormSemestre]) -> TableEtud:
"""Table des étudiants des formsemestres indiqués"""
if not formsemestres:
raise ScoValueError("Aucun semestre en cours")
table = TableEtudWithInfos(row_class=RowEtudWithInfos)
for formsemestre in formsemestres:
table.add_formsemestre(formsemestre)
return table

View File

@ -5,7 +5,7 @@ from datetime import datetime
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif
from flask_sqlalchemy.query import Query, Pagination from flask_sqlalchemy.query import Query, Pagination
from sqlalchemy import union, literal, select, desc from sqlalchemy import union, literal, select, desc
from app import db from app import db, g
from flask import url_for from flask import url_for
from app import log from app import log
@ -16,14 +16,14 @@ class ListeAssiJusti(tb.Table):
L'affichage par défaut se fait par ordre de date de fin décroissante. L'affichage par défaut se fait par ordre de date de fin décroissante.
""" """
NB_PAR_PAGE: int = 2 NB_PAR_PAGE: int = 25
MAX_PAR_PAGE: int = 200
def __init__( def __init__(
self, self,
*etudiants: tuple[Identite], table_data: "Data",
filtre: "Filtre" = None, filtre: "Filtre" = None,
page: int = 1, options: "Options" = None,
nb_par_page: int = None,
**kwargs, **kwargs,
) -> None: ) -> None:
""" """
@ -33,14 +33,12 @@ class ListeAssiJusti(tb.Table):
filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None. filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None.
page (int, optional): numéro de page de la pagination. Defaults to 1. page (int, optional): numéro de page de la pagination. Defaults to 1.
""" """
self.etudiants = etudiants self.table_data: "Data" = table_data
# Gestion du filtre, par défaut un filtre vide # Gestion du filtre, par défaut un filtre vide
self.filtre = filtre if filtre is not None else Filtre() self.filtre = filtre if filtre is not None else Filtre()
# Gestion de la pagination (par défaut page 1)
self.page: int = page # Gestion des options, par défaut un objet Options vide
self.nb_par_page: int = ( self.options = options if options is not None else Options()
nb_par_page if nb_par_page is not None else ListeAssiJusti.NB_PAR_PAGE
)
self.total_page: int = None self.total_page: int = None
@ -57,9 +55,6 @@ class ListeAssiJusti(tb.Table):
self.ajouter_lignes() self.ajouter_lignes()
def etudiant_seul(self) -> bool:
return len(self.etudiants) == 1
def ajouter_lignes(self): def ajouter_lignes(self):
# Générer les query assiduités et justificatifs # Générer les query assiduités et justificatifs
assiduites_query_etudiants: Query = None assiduites_query_etudiants: Query = None
@ -69,13 +64,21 @@ class ListeAssiJusti(tb.Table):
type_obj = self.filtre.type_obj() type_obj = self.filtre.type_obj()
if type_obj in [0, 1]: if type_obj in [0, 1]:
assiduites_query_etudiants = Assiduite.query.filter( assiduites_query_etudiants = self.table_data.assiduites_query
Assiduite.etudid.in_([e.etudid for e in self.etudiants])
) # Non affichage des présences
if not self.options.show_pres:
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.PRESENT
)
# Non affichage des retards
if not self.options.show_reta:
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.RETARD
)
if type_obj in [0, 2]: if type_obj in [0, 2]:
justificatifs_query_etudiants = Justificatif.query.filter( justificatifs_query_etudiants = self.table_data.justificatifs_query
Justificatif.etudid.in_([e.etudid for e in self.etudiants])
)
# Combinaison des requêtes # Combinaison des requêtes
@ -112,7 +115,7 @@ class ListeAssiJusti(tb.Table):
résultats paginés. résultats paginés.
""" """
return query.paginate( return query.paginate(
page=self.page, per_page=self.nb_par_page, error_out=False page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False
) )
def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None): def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None):
@ -149,7 +152,7 @@ class ListeAssiJusti(tb.Table):
# Définir les colonnes pour la requête d'assiduité # Définir les colonnes pour la requête d'assiduité
if query_assiduite: if query_assiduite:
query_assiduite = query_assiduite.with_entities( assiduites_entities: list = [
Assiduite.assiduite_id.label("obj_id"), Assiduite.assiduite_id.label("obj_id"),
Assiduite.etudid.label("etudid"), Assiduite.etudid.label("etudid"),
Assiduite.entry_date.label("entry_date"), Assiduite.entry_date.label("entry_date"),
@ -159,12 +162,17 @@ class ListeAssiJusti(tb.Table):
literal("assiduite").label("type"), literal("assiduite").label("type"),
Assiduite.est_just.label("est_just"), Assiduite.est_just.label("est_just"),
Assiduite.user_id.label("user_id"), Assiduite.user_id.label("user_id"),
) ]
if self.options.show_desc:
assiduites_entities.append(Assiduite.description.label("description"))
query_assiduite = query_assiduite.with_entities(*assiduites_entities)
queries.append(query_assiduite) queries.append(query_assiduite)
# Définir les colonnes pour la requête de justificatif # Définir les colonnes pour la requête de justificatif
if query_justificatif: if query_justificatif:
query_justificatif = query_justificatif.with_entities( justificatifs_entities: list = [
Justificatif.justif_id.label("obj_id"), Justificatif.justif_id.label("obj_id"),
Justificatif.etudid.label("etudid"), Justificatif.etudid.label("etudid"),
Justificatif.entry_date.label("entry_date"), Justificatif.entry_date.label("entry_date"),
@ -176,6 +184,13 @@ class ListeAssiJusti(tb.Table):
# donc on la met en nul car un justifcatif ne peut être justifié # donc on la met en nul car un justifcatif ne peut être justifié
literal(None).label("est_just"), literal(None).label("est_just"),
Justificatif.user_id.label("user_id"), Justificatif.user_id.label("user_id"),
]
if self.options.show_desc:
justificatifs_entities.append(Justificatif.raison.label("description"))
query_justificatif = query_justificatif.with_entities(
*justificatifs_entities
) )
queries.append(query_justificatif) queries.append(query_justificatif)
@ -206,8 +221,8 @@ class RowAssiJusti(tb.Row):
def ajouter_colonnes(self, lien_redirection: str = None): def ajouter_colonnes(self, lien_redirection: str = None):
# Ajout de l'étudiant # Ajout de l'étudiant
self.table: ListeAssiJusti self.table: ListeAssiJusti
if not self.table.etudiant_seul(): if self.table.options.show_etu:
self._etud() self._etud(lien_redirection)
# Type d'objet # Type d'objet
self._type() self._type()
@ -218,28 +233,37 @@ class RowAssiJusti(tb.Row):
"Date de début", "Date de début",
self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"), self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["date_debut"]}, data={"order": self.ligne["date_debut"]},
raw_content=self.ligne["date_debut"],
) )
# Date de fin # Date de fin
self.add_cell( self.add_cell(
"date_fin", "date_fin",
"Date de fin", "Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"), self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
raw_content=self.ligne["date_fin"],
data={"order": self.ligne["date_fin"]}, data={"order": self.ligne["date_fin"]},
) )
# Ajout des colonnes optionnelles
self._optionnelles()
# Ajout colonne actions
if self.table.options.show_actions:
self._actions()
# Ajout de l'utilisateur ayant saisie l'objet
self._utilisateur()
# Date de saisie # Date de saisie
self.add_cell( self.add_cell(
"entry_date", "entry_date",
"Saisie le", "Saisie le",
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"), self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["entry_date"]}, data={"order": self.ligne["entry_date"]},
raw_content=self.ligne["entry_date"],
classes=["small-font"],
) )
# Ajout de l'utilisateur ayant saisie l'objet
self._utilisateur()
# Ajout colonne actions
self._actions()
def _type(self) -> None: def _type(self) -> None:
obj_type: str = "" obj_type: str = ""
is_assiduite: bool = self.ligne["type"] == "assiduite" is_assiduite: bool = self.ligne["type"] == "assiduite"
@ -264,7 +288,7 @@ class RowAssiJusti(tb.Row):
self.add_cell("obj_type", "Type", obj_type) self.add_cell("obj_type", "Type", obj_type)
def _etud(self) -> None: def _etud(self, lien_redirection) -> None:
etud = self.etud etud = self.etud
self.table.group_titles.update( self.table.group_titles.update(
{ {
@ -297,6 +321,21 @@ class RowAssiJusti(tb.Row):
target_attrs={"class": "discretelink"}, target_attrs={"class": "discretelink"},
) )
def _optionnelles(self) -> None:
if self.table.options.show_desc:
self.add_cell(
"description",
"Description",
self.ligne["description"] if self.ligne["description"] else "",
)
if self.table.options.show_module:
if self.ligne["type"] == "assiduite":
assi: Assiduite = Assiduite.query.get(self.ligne["obj_id"])
mod: str = assi.get_module(True)
self.add_cell("module", "Module", mod, data={"order": mod})
else:
self.add_cell("module", "Module", "", data={"order": ""})
def _utilisateur(self) -> None: def _utilisateur(self) -> None:
utilisateur: User = User.query.get(self.ligne["user_id"]) utilisateur: User = User.query.get(self.ligne["user_id"])
@ -304,11 +343,44 @@ class RowAssiJusti(tb.Row):
"user", "user",
"Saisie par", "Saisie par",
"Inconnu" if utilisateur is None else utilisateur.get_nomprenom(), "Inconnu" if utilisateur is None else utilisateur.get_nomprenom(),
classes=["small-font"],
) )
def _actions(self) -> None: def _actions(self) -> None:
# XXX Ajouter une colonne avec les liens d'action (supprimer, modifier) url: str
pass html: list[str] = []
# Détails
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="details",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a title="Détails" href="{url}"></a>') # utiliser url_for
# Modifier
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="modifier",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a title="Modifier" href="{url}">📝</a>') # utiliser url_for
# Supprimer
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="supprimer",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a title="Supprimer" href="{url}">❌</a>') # utiliser url_for
self.add_cell("actions", "Actions", "&ensp;".join(html), no_excel=True)
class Filtre: class Filtre:
@ -323,7 +395,6 @@ class Filtre:
entry_date: tuple[int, datetime] = None, entry_date: tuple[int, datetime] = None,
date_debut: tuple[int, datetime] = None, date_debut: tuple[int, datetime] = None,
date_fin: tuple[int, datetime] = None, date_fin: tuple[int, datetime] = None,
etats: list[EtatAssiduite | EtatJustificatif] = None,
) -> None: ) -> None:
""" """
__init__ Instancie un nouvel objet filtre. __init__ Instancie un nouvel objet filtre.
@ -336,7 +407,7 @@ class Filtre:
etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None. etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None.
""" """
self.filtres = {} self.filtres = {"type_obj": type_obj}
if entry_date is not None: if entry_date is not None:
self.filtres["entry_date"]: tuple[int, datetime] = entry_date self.filtres["entry_date"]: tuple[int, datetime] = entry_date
@ -347,9 +418,6 @@ class Filtre:
if date_fin is not None: if date_fin is not None:
self.filtres["date_fin"]: tuple[int, datetime] = date_fin self.filtres["date_fin"]: tuple[int, datetime] = date_fin
if etats is not None:
self.filtres["etats"]: list[int | EtatJustificatif | EtatAssiduite] = etats
def filtrage(self, query: Query, obj_class: db.Model) -> Query: def filtrage(self, query: Query, obj_class: db.Model) -> Query:
""" """
filtrage Filtre la query passée en paramètre et retourne l'objet filtré filtrage Filtre la query passée en paramètre et retourne l'objet filtré
@ -405,3 +473,64 @@ class Filtre:
int: le/les types d'objets à afficher int: le/les types d'objets à afficher
""" """
return self.filtres.get("type_obj", 0) return self.filtres.get("type_obj", 0)
class Options:
VRAI = ["on", "true", "t", "v", "vrai", True, 1]
def __init__(
self,
page: int = 1,
nb_ligne_page: int = None,
show_pres: str | bool = False,
show_reta: str | bool = False,
show_desc: str | bool = False,
show_etu: str | bool = True,
show_actions: str | bool = True,
show_module: str | bool = False,
):
self.page: int = page
self.nb_ligne_page: int = nb_ligne_page
if self.nb_ligne_page is not None:
self.nb_ligne_page = min(nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE)
self.show_pres: bool = show_pres in Options.VRAI
self.show_reta: bool = show_reta in Options.VRAI
self.show_desc: bool = show_desc in Options.VRAI
self.show_etu: bool = show_etu in Options.VRAI
self.show_actions: bool = show_actions in Options.VRAI
self.show_module: bool = show_module in Options.VRAI
def remplacer(self, **kwargs):
for k, v in kwargs.items():
if k.startswith("show_"):
setattr(self, k, v in Options.VRAI)
elif k in ["page", "nb_ligne_page"]:
setattr(self, k, int(v))
if k == "nb_ligne_page":
self.nb_ligne_page = min(
self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE
)
class Data:
def __init__(
self, assiduites_query: Query = None, justificatifs_query: Query = None
):
self.assiduites_query: Query = assiduites_query
self.justificatifs_query: Query = justificatifs_query
@staticmethod
def from_etudiants(*etudiants: Identite) -> "Data":
data = Data()
data.assiduites_query = Assiduite.query.filter(
Assiduite.etudid.in_([e.etudid for e in etudiants])
)
data.justificatifs_query = Justificatif.query.filter(
Justificatif.etudid.in_([e.etudid for e in etudiants])
)
return data
def get(self) -> tuple[Query, Query]:
return self.assiduites_query, self.justificatifs_query

View File

@ -84,6 +84,8 @@ class Table(Element):
self.row_by_id: dict[str, "Row"] = {} self.row_by_id: dict[str, "Row"] = {}
self.column_ids = [] self.column_ids = []
"ordered list of columns ids" "ordered list of columns ids"
self.raw_column_ids = []
"ordered list of columns ids for excel"
self.groups = [] self.groups = []
"ordered list of column groups names" "ordered list of column groups names"
self.group_titles = {} self.group_titles = {}
@ -360,6 +362,7 @@ class Row(Element):
target_attrs: dict = None, target_attrs: dict = None,
target: str = None, target: str = None,
column_classes: set[str] = None, column_classes: set[str] = None,
no_excel: bool = False,
) -> "Cell": ) -> "Cell":
"""Create cell and add it to the row. """Create cell and add it to the row.
group: groupe de colonnes group: groupe de colonnes
@ -380,10 +383,17 @@ class Row(Element):
target=target, target=target,
target_attrs=target_attrs, target_attrs=target_attrs,
) )
return self.add_cell_instance(col_id, cell, column_group=group, title=title) return self.add_cell_instance(
col_id, cell, column_group=group, title=title, no_excel=no_excel
)
def add_cell_instance( def add_cell_instance(
self, col_id: str, cell: "Cell", column_group: str = None, title: str = None self,
col_id: str,
cell: "Cell",
column_group: str = None,
title: str = None,
no_excel: bool = False,
) -> "Cell": ) -> "Cell":
"""Add a cell to the row. """Add a cell to the row.
Si title est None, il doit avoir été ajouté avec table.add_title(). Si title est None, il doit avoir été ajouté avec table.add_title().
@ -392,6 +402,9 @@ class Row(Element):
self.cells[col_id] = cell self.cells[col_id] = cell
if col_id not in self.table.column_ids: if col_id not in self.table.column_ids:
self.table.column_ids.append(col_id) self.table.column_ids.append(col_id)
if not no_excel:
self.table.raw_column_ids.append(col_id)
self.table.insert_group(column_group) self.table.insert_group(column_group)
if column_group is not None: if column_group is not None:
self.table.column_group[col_id] = column_group self.table.column_group[col_id] = column_group
@ -422,7 +435,7 @@ class Row(Element):
"""row as a dict, with only cell contents""" """row as a dict, with only cell contents"""
return { return {
col_id: self.cells.get(col_id, self.table.empty_cell).raw_content col_id: self.cells.get(col_id, self.table.empty_cell).raw_content
for col_id in self.table.column_ids for col_id in self.table.raw_column_ids
} }
def to_excel(self, sheet, style=None) -> list: def to_excel(self, sheet, style=None) -> list:

View File

@ -1,14 +1,15 @@
{% include "assiduites/widgets/toast.j2" %} {% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% block pageContent %} {% block pageContent %}
<div class="pageContent"> <div class="pageContent">
<h3>Ajouter une assiduité</h3> <h3>Signaler un évènement pour {{etud.html_link_fiche()|safe}}</h3>
{% include "assiduites/widgets/tableau_base.j2" %}
{% if saisie_eval %} {% if saisie_eval %}
<div id="saisie_eval"> <div id="saisie_eval">
<br> <br>
<h3> <h3>
La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation. <br> La saisie a été préconfigurée en fonction de l'évaluation. <br>
Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation Une fois la saisie terminée, cliquez sur le lien ci-dessous
</h3> </h3>
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a> <a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
</div> </div>
@ -29,7 +30,7 @@
<div class="assi-row"> <div class="assi-row">
<div class="assi-label"> <div class="assi-label">
<legend for="assi_etat" required>Etat de l'assiduité</legend> <legend for="assi_etat" required>État de l'assiduité</legend>
<select name="assi_etat" id="assi_etat"> <select name="assi_etat" id="assi_etat">
<option value="absent" selected>Absent</option> <option value="absent" selected>Absent</option>
<option value="retard">Retard</option> <option value="retard">Retard</option>
@ -54,7 +55,7 @@
</div> </div>
<div class="assi-row"> <div class="assi-row">
<button onclick="validerFormulaire(this)">Créer l'assiduité</button> <button onclick="validerFormulaire(this)">Enregistrer</button>
<button onclick="effacerFormulaire()">Remettre à zero</button> <button onclick="effacerFormulaire()">Remettre à zero</button>
</div> </div>
@ -63,8 +64,7 @@
</section> </section>
<section class="liste"> <section class="liste">
<a class="icon filter" onclick="filterAssi()"></a> {{tableau | safe }}
{% include "assiduites/widgets/tableau_assi.j2" %}
</section> </section>
</div> </div>
@ -141,7 +141,7 @@
let assiduite_id = null; let assiduite_id = null;
createAssiduiteComplete(assiduite, etudid); createAssiduiteComplete(assiduite, etudid);
loadAll(); updateTableau();
btn.disabled = true; btn.disabled = true;
setTimeout(() => { setTimeout(() => {
btn.disabled = false; btn.disabled = false;
@ -208,7 +208,6 @@
{% endif %} {% endif %}
window.addEventListener("load", () => { window.addEventListener("load", () => {
loadAll();
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() }); document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
dayOnly() dayOnly()
@ -231,4 +230,4 @@
}); });
</script> </script>
{% endblock pageContent %} {% endblock pageContent %}

View File

@ -2,8 +2,6 @@
{% block pageContent %} {% block pageContent %}
<div class="pageContent"> <div class="pageContent">
<h3>Justifier des absences ou retards</h3> <h3>Justifier des absences ou retards</h3>
{% include "assiduites/widgets/tableau_base.j2" %}
<section class="justi-form page"> <section class="justi-form page">
@ -58,28 +56,9 @@
</section> </section>
<section class="liste"> <section class="liste">
<a class="icon filter" onclick="filterJusti()"></a> {{tableau | safe }}
{% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
<div class="legende">
<h3>Gestion des justificatifs</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
<ul>
<li>Détails : Affiche les détails du justificatif sélectionné</li>
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
</ul>
</p>
<p>Cliquer sur l'icone d'entonoir afin de filtrer le tableau des justificatifs</p>
</div>
</div> </div>
<style> <style>
@ -167,17 +146,15 @@
processData: false, processData: false,
success: () => { success: () => {
pushToast(generateToast(document.createTextNode(`Importation du fichier : ${f.name} finie`))); pushToast(generateToast(document.createTextNode(`Importation du fichier : ${f.name} finie`)));
loadAll();
}, },
} }
) )
) )
}); });
if (in_files.files.length == 0) { $.when(...requests).done(() => {
loadAll(); location.reload();
} })
} }
function validerFormulaire(btn) { function validerFormulaire(btn) {
@ -258,7 +235,6 @@
const assi_evening = '{{assi_evening}}'; const assi_evening = '{{assi_evening}}';
window.onload = () => { window.onload = () => {
loadAll();
document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() }); document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() });
dayOnly() dayOnly()

View File

@ -2,95 +2,6 @@
<div class="pageContent"> <div class="pageContent">
<h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2> <h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
{% include "assiduites/widgets/tableau_base.j2" %} {{tableau | safe }}
<h3>Assiduité :</h3>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<h3>Justificatifs :</h3>
<span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
<ul id="contextMenu" class="context-menu">
<li id="detailOption">Detail</li>
<li id="editOption">Editer</li>
<li id="deleteOption">Supprimer</li>
</ul>
<div class="legende">
<h3>Gestion des justificatifs</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
</p>
<ul>
<li>Détails : Affiche les détails du justificatif sélectionné</li>
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li>
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
</ul>
<p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonnoir sous le titre du tableau.</p>
<h3>Gestion de l'assiduité</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
</p>
<ul>
<li>Détails : affiche les détails de l'assiduité sélectionnée</li>
<li>Éditer : modifier l'élément (module, état)</li>
<li>Supprimer : supprimer l'élément (action irréversible)</li>
</ul>
<p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonnoir sous le titre du tableau.</p>
</div>
</div> </div>
{% endblock app_content %} {% endblock app_content %}
<script>
const etudid = {{ sco.etud.id }}
const assiduite_unique_id = {{ assi_id }};
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
function wayForFilter() {
if (typeof assiduites[etudid] !== "undefined") {
console.log("Done")
let assiduite = assiduites[etudid].filter((a) => { return a.assiduite_id == assiduite_unique_id });
if (assiduite) {
assiduite = assiduite[0]
filterAssiduites["filters"] = {
"obj_id": [
assiduite.assiduite_id,
]
}
const obj_ids = assiduite.justificatifs ? assiduite.justificatifs.map((j) => { return j.justif_id }) : []
filterJustificatifs["filters"] = {
"obj_id": obj_ids
}
loadAll();
}
} else {
setTimeout(wayForFilter, 250)
}
}
window.onload = () => {
loadAll();
if (assiduite_unique_id != -1) {
wayForFilter()
}
}
</script>

View File

@ -0,0 +1,27 @@
{% extends "sco_page.j2" %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %}
{% block app_content %}
{% if action == "modifier" %}
{% include "assiduites/widgets/tableau_actions/modifier.j2" %}
{% else%}
{% include "assiduites/widgets/tableau_actions/details.j2" %}
{% endif %}
<br>
<hr>
<br>
<a href="" id="lien-retour">Retour</a>
<script>
window.addEventListener('load', () => {
document.getElementById("lien-retour").href = document.referrer;
})
</script>
{% endblock %}

View File

@ -1,44 +0,0 @@
{% extends "sco_page.j2" %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock %}
{% block app_content %}
<legend>
Options
<form action="" method="get">
<label for="show_pres">afficher les présences</label>
{% if show_pres %}
<input type="checkbox" id="show_pres" name="show_pres" checked>
{% else %}
<input type="checkbox" id="show_pres" name="show_pres">
{% endif %}
<label for="show_reta">afficher les retards</label>
{% if show_reta %}
<input type="checkbox" id="show_reta" name="show_reta" checked>
{% else %}
<input type="checkbox" id="show_reta" name="show_reta">
{% endif %}
<br>
<label for="nb_ligne_page">Nombre de ligne par page : </label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page" value="{{nb_ligne_page}}">
<label for="n_page">Page n°</label>
<select name="n_page" id="n_page">
{% for n in range(1,total_pages+1) %}
<option value="{{n}}">{{n}}</option>
{% endfor %}
</select>
<br>
<input type="submit" value="valider">
</form>
</legend>
{{tableau | safe}}
{% endblock %}

View File

@ -3,6 +3,7 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script> <script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %} {% endblock %}
{% block app_content %} {% block app_content %}
@ -43,4 +44,4 @@
</script> </script>
{% endblock %} {% endblock %}

View File

@ -119,11 +119,17 @@
} }
{% if moduleid %}
const moduleimpl_dynamic_selector_id = "{{moduleid}}"
{% else %}
const moduleimpl_dynamic_selector_id = "moduleimpl_select"
{% endif %}
window.addEventListener("load", () => { window.addEventListener("load", () => {
document.getElementById('moduleimpl_select').addEventListener('change', (el) => { document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => {
const assi = getCurrentAssiduite(etudid); const assi = getCurrentAssiduite(etudid);
if (assi) { if (assi) {
editAssiduite(assi.assiduite_id, assi.etat, [assi]); editAssiduite(assi.assiduite_id, assi.etat, [assi]);

View File

@ -1,6 +1,8 @@
<select name="moduleimpl_select" id="moduleimpl_select"> <select name="moduleimpl_select" id="moduleimpl_select">
{% with moduleimpl_id=moduleimpl_id %}
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %} {% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
{% endwith %}
{% for mod in modules %} {% for mod in modules %}
{% if mod.moduleimpl_id == moduleimpl_id %} {% if mod.moduleimpl_id == moduleimpl_id %}

View File

@ -1,6 +1,10 @@
{% if scu.is_assiduites_module_forced(request.args.get('formsemestre_id', None))%} {% if scu.is_assiduites_module_forced(request.args.get('formsemestre_id', None))%}
<option value="" selected disabled> Saisir Module</option> <option value="" disabled> Saisir Module</option>
{% else %} {% else %}
<option value="" selected> Non spécifié </option> <option value=""> Non spécifié </option>
{% endif %} {% endif %}
<option value="autre"> Tout module </option> {% if moduleimpl_id == "autre" %}
<option value="autre" selected> Tout module </option>
{% else %}
<option value="autre"> Tout module </option>
{% endif %}

View File

@ -0,0 +1,73 @@
<hr>
<div>
<h3>Options</h3>
<div id="options-tableau">
{% if afficher_options != false %}
<label for="show_pres">afficher les présences</label>
{% if options.show_pres %}
<input type="checkbox" id="show_pres" name="show_pres" checked>
{% else %}
<input type="checkbox" id="show_pres" name="show_pres">
{% endif %}
<label for="show_reta">afficher les retards</label>
{% if options.show_reta %}
<input type="checkbox" id="show_reta" name="show_reta" checked>
{% else %}
<input type="checkbox" id="show_reta" name="show_reta">
{% endif %}
<label for="with_desc">afficher les descriptions</label>
{% if options.show_desc %}
<input type="checkbox" id="show_desc" name="show_desc" checked>
{% else %}
<input type="checkbox" id="show_desc" name="show_desc">
{% endif %}
<br>
{% endif %}
<label for="nb_ligne_page">Nombre de ligne par page : </label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page" value="{{options.nb_ligne_page}}">
<label for="n_page">Page n°</label>
<select name="n_page" id="n_page">
{% for n in range(1,total_pages+1) %}
{% if n == options.page %}
<option value="{{n}}" selected>{{n}}</option>
{% else %}
<option value="{{n}}">{{n}}</option>
{% endif %}
{% endfor %}
</select>
<br>
<button onclick="updateTableau()">valider</button>
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
</div>
</div>
{{tableau | safe}}
<script>
function updateTableau() {
const url = new URL(location.href);
const form = document.getElementById("options-tableau");
const formValues = form.querySelectorAll("*[name]");
formValues.forEach((el) => {
if (el.type == "checkbox") {
url.searchParams.set(el.name, el.checked)
} else {
url.searchParams.set(el.name, el.value)
}
})
location.href = url.href;
}
</script>
<style>
.small-font {
font-size: 9pt;
}
</style>

View File

@ -0,0 +1,106 @@
<h1>Détails {{type}} </h1>
<div id="informations">
<div class="info-row">
<span class="info-label">Étudiant.e concerné.e:</span> <span class="etudinfo"
id="etudid-{{objet.etudid}}">{{objet.etud_nom}}</span>
</div>
<div class="info-row">
<span class="info-label">Période concernée :</span> {{objet.date_debut}} au {{objet.date_fin}}
</div>
{% if type == "Assiduité" %}
<div class="info-row">
<span class="info-label">Module concernée :</span> {{objet.module}}
</div>
{% else %}
{% endif %}
<div class="info-row">
{% if type == "Justificatif" %}
<span class="info-label">État du justificatif :</span>
{% else %}
<span class="info-label">État de l'assiduité :</span>
{% endif %}
{{objet.etat}}
</div>
<div class="info-row">
{% if type == "Justificatif" %}
<div class="info-label">Raison:</div>
{% if objet.raison != None %}
<div class="text">{{objet.raison}}</div>
{% else %}
<div class="text">/div>
{% endif %}
{% else %}
<div class="info-label">Description:</div>
{% if objet.description != None %}
<div class="text">{{objet.description}}</div>
{% else %}
<div class="text"></div>
{% endif %}
{% endif %}
</div>
</div>
{# Affichage des justificatifs si assiduité justifiée #}
{% if type == "Assiduité" and objet.etat != "Présence" %}
<div class="info-row">
<span class="info-label">Justifiée: </span>
{% if objet.justification.est_just %}
<span class="text">Oui</span>
<div>
{% for justi in objet.justification.justificatifs %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
{% endfor %}
</div>
{% else %}
<span class="text">Non</span>
{% endif %}
</div>
{% endif %}
{# Affichage des assiduités justifiées si justificatif valide #}
{% if type == "Justificatif" and objet.etat == "Valide" %}
<div class="info-row">
<span class="info-label">Assiduités concernées: </span>
{% if objet.justification.assiduites %}
<div>
{% for assi in objet.justification.assiduites %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
{{assi.date_fin}}</a>
{% endfor %}
</div>
{% else %}
<span class="text">Aucune</span>
{% endif %}
</div>
{% endif %}
{# Affichage des fichiers des justificatifs #}
{% if type == "Justificatif"%}
<div class="info-row">
<span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li><a
href="{{url_for('api.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</div>
{% endif %}
<div class="info-row">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>

View File

@ -0,0 +1,107 @@
<h1>Modifier {{type}} </h1>
<form action="" method="post" enctype="multipart/form-data">
<input type="hidden" name="obj_id" value="{{obj_id}}">
<input type="hidden" name="table_url" id="table_url" value="">
{% if type == "Assiduité" %}
<input type="hidden" name="obj_type" value="assiduite">
<legend for="etat">État</legend>
<select name="etat" id="etat">
<option value="absent">Absent</option>
<option value="retard">Retard</option>
<option value="present">Présent</option>
</select>
<legend for="moduleimpl_select">Module</legend>
{{moduleimpl | safe}}
<legend for="description">Description</legend>
<textarea name="description" id="description" cols="50" rows="5">{{objet.description}}</textarea>
{% else %}
<input type="hidden" name="obj_type" value="justificatif">
<legend for="date_debut">Date de début</legend>
<scodoc-datetime name="date_debut" id="date_debut" value="{{objet.real_date_debut}}"></scodoc-datetime>
<legend for="date_fin">Date de fin</legend>
<scodoc-datetime name="date_fin" id="date_fin" value="{{objet.real_date_fin}}"></scodoc-datetime>
<legend for="etat">État</legend>
<select name="etat" id="etat">
<option value="valide">Valide</option>
<option value="non_valide">Non Valide</option>
<option value="attente">En Attente</option>
<option value="modifie">Modifié</option>
</select>
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
<legend>Fichiers</legend>
<div class="info-row">
<label class="info-label">Fichiers enregistrés: </label>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li data-id="{{filename}}">
<a data-file="{{filename}}">❌</a>
<a data-link=""
href="{{url_for('api.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}"><span
data-file="{{filename}}">{{filename}}</span></a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</div>
<br>
<label for="justi_fich">Ajouter des fichiers:</label>
<input type="file" name="justi_fich" id="justi_fich" multiple>
{% endif %}
<br>
<br>
<input type="submit" value="Valider">
</form>
<script>
function removeFile(element) {
const link = document.querySelector(`*[data-id="${element.getAttribute('data-file')}"] a[data-link] span`);
link?.toggleAttribute("data-remove")
}
function deleteFiles(justif_id) {
const filenames = Array.from(document.querySelectorAll("*[data-remove]")).map((el) => el.getAttribute("data-file"))
obj = {
"remove": "list",
"filenames": filenames
}
//faire un POST à l'api justificatifs
}
window.addEventListener('load', () => {
document.getElementById('etat').value = "{{objet.real_etat}}";
document.getElementById('table_url').value = document.referrer;
document.querySelectorAll("a[data-file]").forEach((e) => {
e.addEventListener('click', () => {
removeFile(e);
})
})
})
</script>
<style>
[data-remove] {
text-decoration: line-through;
}
[data-file] {
cursor: pointer;
user-select: none;
}
</style>

View File

@ -9,15 +9,18 @@
<div class="user_basics"> <div class="user_basics">
<b>Login :</b> {{user.user_name}}<br> <b>Login :</b> {{user.user_name}}<br>
<b>CAS id:</b> {{user.cas_id or "(aucun)"}} <b>CAS id:</b> {{user.cas_id or "(aucun)"}}
(CAS {{'autorisé' if user.cas_allow_login else 'interdit'}} pour cet utilisateur) {% if ScoDocSiteConfig.is_cas_enabled() %}
{% if user.cas_allow_scodoc_login %} (CAS {{'autorisé' if user.cas_allow_login else 'interdit'}} pour cet utilisateur)
(connexion sans CAS autorisée) {% if user.cas_allow_scodoc_login %}
(connexion sans CAS autorisée)
{% endif %}
{% endif %} {% endif %}
<br> <br>
<b>Nom :</b> {{user.nom or ""}}<br> <b>Nom :</b> {{user.nom or ""}}<br>
<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 %}

View File

@ -0,0 +1,20 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<div class="tab-content">
<h2>Étudiants des semestres courants</h2>
<a href="{{
url_for('scolar.export_etudiants_courants', scodoc_dept=g.scodoc_dept, fmt='xls')
}}">{{scu.ICON_XLS|safe}}</a>
{{ table.html() | safe }}
</div>
{% endblock %}

View File

@ -617,7 +617,7 @@
listeGroupesAutoaffectation(); listeGroupesAutoaffectation();
}) })
.catch(error => { .catch(error => {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (1).</h2>";
}) })
} }
@ -665,13 +665,13 @@
document.querySelector(`#zoneGroupes .partition[data-idpartition="${idPartition}"]`).innerHTML += templateGroupe_zoneGroupes(r.id, name); document.querySelector(`#zoneGroupes .partition[data-idpartition="${idPartition}"]`).innerHTML += templateGroupe_zoneGroupes(r.id, name);
// Lancement de l'édition du nom // Lancement de l'édition du nom
divGroupe.querySelector(".modif").click(); // divGroupe.querySelector(".modif").click();
listeGroupesAutoaffectation(); listeGroupesAutoaffectation();
}) })
.catch(error => { .catch(error => {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (4).</h2>";
}) });
} }
/********************/ /********************/
@ -746,12 +746,12 @@
.then(r => { return r.json() }) .then(r => { return r.json() })
.then(r => { .then(r => {
if (!r) { if (!r) {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (2).</h2>";
} }
listeGroupesAutoaffectation(); listeGroupesAutoaffectation();
}) })
.catch(error => { .catch(error => {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (3).</h2>";
}) })
} }
@ -802,7 +802,7 @@
.then(r => { return r.json() }) .then(r => { return r.json() })
.then(r => { .then(r => {
if (r.OK != true) { if (r.OK != true) {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (5).</h2>";
} }
listeGroupesAutoaffectation(); listeGroupesAutoaffectation();
}) })
@ -916,12 +916,12 @@
.then(r => { return r.json() }) .then(r => { return r.json() })
.then(r => { .then(r => {
if (!r) { if (!r) {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (6).</h2>";
} }
listeGroupesAutoaffectation(); listeGroupesAutoaffectation();
}) })
.catch(error => { .catch(error => {
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>"; document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (7).</h2>";
}) })
} }

View File

@ -32,6 +32,7 @@ from flask import abort, url_for, redirect
from flask_login import current_user from flask_login import current_user
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.decorators import ( from app.decorators import (
@ -47,6 +48,10 @@ from app.models import (
Departement, Departement,
Evaluation, Evaluation,
) )
from app.auth.models import User
from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified
import app.tables.liste_assiduites as liste_assi
from app.views import assiduites_bp as bp from app.views import assiduites_bp as bp
from app.views import ScoData from app.views import ScoData
@ -65,6 +70,7 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -260,13 +266,6 @@ def signal_assiduites_etud():
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Récupération de la date (par défaut la date du jour)
date = request.args.get("date", datetime.date.today().isoformat())
heures: list[str] = [
request.args.get("heure_deb", ""),
request.args.get("heure_fin", ""),
]
# gestion évaluations (Appel à la page depuis les évaluations) # gestion évaluations (Appel à la page depuis les évaluations)
saisie_eval: bool = request.args.get("saisie_eval") is not None saisie_eval: bool = request.args.get("saisie_eval") is not None
@ -299,21 +298,17 @@ def signal_assiduites_etud():
], ],
) )
# Gestion des horaires (journée, matin, soir) tableau = _preparer_tableau(
liste_assi.Data.from_etudiants(
morning = ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00") etud,
lunch = ScoDocSiteConfig.assi_get_rounded_time("assi_lunch_time", "13:00:00") ),
afternoon = ScoDocSiteConfig.assi_get_rounded_time( filename=f"assiduite-{etudid}",
"assi_afternoon_time", "18:00:00" afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=1),
options=liste_assi.Options(show_module=True),
) )
if not tableau[0]:
# Gestion du selecteur de moduleimpl (pour le tableau différé) return tableau[1]
select = f"""
<select class="dynaSelect">
{render_template("assiduites/widgets/simplemoduleimpl_select.j2")}
</select>
"""
# Génération de la page # Génération de la page
return HTMLBuilder( return HTMLBuilder(
header, header,
@ -330,8 +325,10 @@ def signal_assiduites_etud():
saisie_eval=saisie_eval, saisie_eval=saisie_eval,
date_deb=date_deb, date_deb=date_deb,
date_fin=date_fin, date_fin=date_fin,
etud=etud,
redirect_url=redirect_url, redirect_url=redirect_url,
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
tableau=tableau[1],
), ),
# render_template( # render_template(
# "assiduites/pages/signal_assiduites_etud.j2", # "assiduites/pages/signal_assiduites_etud.j2",
@ -377,7 +374,7 @@ def liste_assiduites_etud():
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Gestion d'une assiduité unique (redirigé depuis le calendrier) # Gestion d'une assiduité unique (redirigé depuis le calendrier) TODO-Assiduites
assiduite_id: int = request.args.get("assiduite_id", -1) assiduite_id: int = request.args.get("assiduite_id", -1)
# Préparation de la page # Préparation de la page
@ -393,18 +390,25 @@ def liste_assiduites_etud():
"css/assiduites.css", "css/assiduites.css",
], ],
) )
tableau = _preparer_tableau(
liste_assi.Data.from_etudiants(
etud,
),
filename=f"assiduites-justificatifs-{etudid}",
afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=0),
options=liste_assi.Options(show_module=True),
)
if not tableau[0]:
return tableau[1]
# Peuplement du template jinja # Peuplement du template jinja
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template( render_template(
"assiduites/pages/liste_assiduites.j2", "assiduites/pages/liste_assiduites.j2",
sco=ScoData(etud), sco=ScoData(etud),
date=datetime.date.today().isoformat(),
assi_id=assiduite_id, assi_id=assiduite_id,
assi_limit_annee=sco_preferences.get_preference( tableau=tableau[1],
"assi_limit_annee",
dept_id=g.scodoc_dept_id,
),
), ),
).build() ).build()
@ -501,6 +505,19 @@ def ajout_justificatif_etud():
], ],
) )
tableau = _preparer_tableau(
liste_assi.Data.from_etudiants(
etud,
),
filename=f"justificatifs-{etudid}",
afficher_etu=False,
filtre=liste_assi.Filtre(type_obj=2),
options=liste_assi.Options(show_module=False, show_desc=True),
afficher_options=False,
)
if not tableau[0]:
return tableau[1]
# Peuplement du template jinja # Peuplement du template jinja
return HTMLBuilder( return HTMLBuilder(
header, header,
@ -513,6 +530,7 @@ def ajout_justificatif_etud():
), ),
assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"), assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"), assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),
tableau=tableau[1],
), ),
).build() ).build()
@ -1044,26 +1062,44 @@ def visu_assi_group():
) )
@bp.route("/testTableau") def _preparer_tableau(
@scodoc data: liste_assi.Data,
@permission_required(Permission.ScoView) filename: str = "tableau-assiduites",
def testTableau(): afficher_etu: bool = True,
"""Visualisation de l'assiduité d'un groupe entre deux dates""" filtre: liste_assi.Filtre = None,
options: liste_assi.Options = None,
afficher_options: bool = True,
) -> tuple[bool, "Response"]:
"""
_preparer_tableau prépare un tableau d'assiduités / justificatifs
etudid = request.args.get( Cette fontion récupère dans la requête les arguments :
"etudid", 18114
) # TODO retirer la valeur par défaut de test valeurs possibles des booléens vrais ["on", "true", "t", "v", "vrai", True, 1]
toute autre valeur est considérée comme fausse.
show_pres : bool -> Affiche les présences, par défaut False
show_reta : bool -> Affiche les retard, par défaut False
show_desc : bool -> Affiche les descriptions, par défaut False
Returns:
tuple[bool | "Reponse" ]:
- bool : Vrai si la réponse est du Text/HTML
- Reponse : du Text/HTML ou Une Reponse (téléchargement fichier)
"""
fmt = request.args.get("fmt", "html")
show_pres: bool | str = request.args.get("show_pres", False) show_pres: bool | str = request.args.get("show_pres", False)
show_reta: bool | str = request.args.get("show_reta", False) show_reta: bool | str = request.args.get("show_reta", False)
show_desc: bool | str = request.args.get("show_desc", False)
nb_ligne_page: int = request.args.get("nb_ligne_page") nb_ligne_page: int = request.args.get("nb_ligne_page")
# Vérification de nb_ligne_page # Vérification de nb_ligne_page
try: try:
nb_ligne_page: int = int(nb_ligne_page) nb_ligne_page: int = int(nb_ligne_page)
except (ValueError, TypeError): except (ValueError, TypeError):
nb_ligne_page = None nb_ligne_page = liste_assi.ListeAssiJusti.NB_PAR_PAGE
page_number: int = request.args.get("n_page", 1) page_number: int = request.args.get("n_page", 1)
# Vérification de page_number # Vérification de page_number
@ -1072,33 +1108,272 @@ def testTableau():
except (ValueError, TypeError): except (ValueError, TypeError):
page_number = 1 page_number = 1
from app.tables.liste_assiduites import ListeAssiJusti fmt = request.args.get("fmt", "html")
table: ListeAssiJusti = ListeAssiJusti( if options is None:
Identite.get_etud(etudid), page=page_number, nb_par_page=nb_ligne_page options: liste_assi.Options = liste_assi.Options()
options.remplacer(
page=page_number,
nb_ligne_page=nb_ligne_page,
show_pres=show_pres,
show_reta=show_reta,
show_desc=show_desc,
show_etu=afficher_etu,
)
table: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti(
table_data=data,
options=options,
filtre=filtre,
) )
if fmt.startswith("xls"): if fmt.startswith("xls"):
return scu.send_file( return False, scu.send_file(
table.excel(), table.excel(),
filename=f"assiduite-{groups_infos.groups_filename}", filename=filename,
mime=scu.XLSX_MIMETYPE, mime=scu.XLSX_MIMETYPE,
suffix=scu.XLSX_SUFFIX, suffix=scu.XLSX_SUFFIX,
) )
return render_template( return True, render_template(
"assiduites/pages/test_assi.j2", "assiduites/widgets/tableau.j2",
sco=ScoData(),
tableau=table.html(), tableau=table.html(),
title=f"Test tableau",
total_pages=table.total_pages, total_pages=table.total_pages,
page_number=page_number, options=options,
show_pres=show_pres, afficher_options=afficher_options,
show_reta=show_reta,
nb_ligne_page=nb_ligne_page,
) )
@bp.route("/TableauAssiduiteActions", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.AbsChange)
def tableau_assiduite_actions():
obj_type: str = request.args.get("type", "assiduite")
action: str = request.args.get("action", "details")
obj_id: str = int(request.args.get("obj_id", -1))
objet: Assiduite | Justificatif
if obj_type == "assiduite":
objet: Assiduite = Assiduite.query.get_or_404(obj_id)
else:
objet: Justificatif = Justificatif.query.get_or_404(obj_id)
if action == "supprimer":
objet.supprimer()
if obj_type == "assiduite":
flash("L'assiduité a bien été supprimée")
else:
flash("Le justificatif a bien été supprimé")
return redirect(request.referrer)
if request.method == "GET":
module = ""
if obj_type == "assiduite":
formsemestre = objet.get_formsemestre()
if objet.moduleimpl_id is not None:
module = objet.moduleimpl_id
elif objet.external_data is not None:
module = objet.external_data.get("module", "")
module = module.lower() if isinstance(module, str) else module
module = _module_selector(formsemestre, module)
return render_template(
"assiduites/pages/tableau_actions.j2",
sco=ScoData(etud=objet.etudiant),
type="Justificatif" if obj_type == "justificatif" else "Assiduité",
action=action,
objet=_preparer_objet(obj_type, objet),
obj_id=obj_id,
moduleimpl=module,
)
# Cas des POSTS
if obj_type == "assiduite":
try:
_action_modifier_assiduite(objet)
except ScoValueError as error:
raise ScoValueError(error.args[0], request.referrer) from error
flash("L'assiduité a bien été modifiée.")
else:
try:
_action_modifier_justificatif(objet)
except ScoValueError as error:
raise ScoValueError(error.args[0], request.referrer) from error
flash("Le justificatif a bien été modifié.")
return redirect(request.form["table_url"])
def _action_modifier_assiduite(assi: Assiduite):
form = request.form
# Gestion de l'état
etat = scu.EtatAssiduite.get(form["etat"])
if etat is not None:
assi.etat = etat
if etat == scu.EtatAssiduite.PRESENT:
assi.est_just = False
else:
assi.est_just = len(get_assiduites_justif(assi.assiduite_id, False)) > 0
# Gestion de la description
assi.description = form["description"]
module: str = form["moduleimpl_select"]
if module == "":
module = None
else:
try:
module = int(module)
except ValueError:
pass
assi.set_moduleimpl(module)
db.session.add(assi)
db.session.commit()
scass.simple_invalidate_cache(assi.to_dict(True), assi.etudid)
def _action_modifier_justificatif(justi: Justificatif):
form = request.form
# Gestion des Dates
date_debut: datetime = scu.is_iso_formated(form["date_debut"], True)
date_fin: datetime = scu.is_iso_formated(form["date_fin"], True)
if date_debut is None or date_fin is None or date_fin < date_debut:
raise ScoValueError("Dates invalides", request.referrer)
justi.date_debut = date_debut
justi.date_fin = date_fin
# Gestion de l'état
etat = scu.EtatJustificatif.get(form["etat"])
if etat is not None:
justi.etat = etat
else:
raise ScoValueError("État invalide", request.referrer)
# Gestion de la raison
justi.raison = form["raison"]
# Gestion des fichiers
files = request.files.getlist("justi_fich")
if len(files) != 0:
files = request.files.values()
archive_name: str = justi.fichier
# Utilisation de l'archiver de justificatifs
archiver: JustificatifArchiver = JustificatifArchiver()
for fich in files:
archive_name, _ = archiver.save_justificatif(
justi.etudiant,
filename=fich.filename,
data=fich.stream.read(),
archive_name=archive_name,
user_id=current_user.id,
)
justi.fichier = archive_name
db.session.add(justi)
db.session.commit()
scass.compute_assiduites_justified(justi.etudid, reset=True)
scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid)
def _preparer_objet(
obj_type: str, objet: Assiduite | Justificatif, sans_gros_objet: bool = False
) -> dict:
# Préparation d'un objet pour simplifier l'affichage jinja
objet_prepare: dict = objet.to_dict()
if obj_type == "assiduite":
objet_prepare["etat"] = (
scu.EtatAssiduite(objet.etat).version_lisible().capitalize()
)
objet_prepare["real_etat"] = scu.EtatAssiduite(objet.etat).name.lower()
objet_prepare["description"] = (
"" if objet.description is None else objet.description
)
objet_prepare["description"] = objet_prepare["description"].strip()
# Gestion du module
objet_prepare["module"] = objet.get_module(True)
# Gestion justification
if not objet.est_just:
objet_prepare["justification"] = {"est_just": False}
else:
objet_prepare["justification"] = {"est_just": True, "justificatifs": []}
if not sans_gros_objet:
justificatifs: list[int] = get_assiduites_justif(
objet.assiduite_id, False
)
for justi_id in justificatifs:
justi: Justificatif = Justificatif.query.get(justi_id)
objet_prepare["justification"]["justificatifs"].append(
_preparer_objet("justificatif", justi, sans_gros_objet=True)
)
else:
objet_prepare["etat"] = (
scu.EtatJustificatif(objet.etat).version_lisible().capitalize()
)
objet_prepare["real_etat"] = scu.EtatJustificatif(objet.etat).name.lower()
objet_prepare["raison"] = "" if objet.raison is None else objet.raison
objet_prepare["raison"] = objet_prepare["raison"].strip()
objet_prepare["justification"] = {"assiduites": [], "fichiers": {}}
if not sans_gros_objet:
assiduites: list[int] = scass.justifies(objet)
for assi_id in assiduites:
assi: Assiduite = Assiduite.query.get(assi_id)
objet_prepare["justification"]["assiduites"].append(
_preparer_objet("assiduite", assi, sans_gros_objet=True)
)
# Récupération de l'archive avec l'archiver
archive_name: str = objet.fichier
filenames: list[str] = []
archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None:
filenames = archiver.list_justificatifs(archive_name, objet.etudiant)
objet_prepare["justification"]["fichiers"] = {
"total": len(filenames),
"filenames": [],
}
for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.AbsJustifView
):
objet_prepare["justification"]["fichiers"]["filenames"].append(
filename[0]
)
objet_prepare["date_fin"] = objet.date_fin.strftime("%d/%m/%y à %H:%M")
objet_prepare["real_date_fin"] = objet.date_fin.isoformat()
objet_prepare["date_debut"] = objet.date_debut.strftime("%d/%m/%y à %H:%M")
objet_prepare["real_date_debut"] = objet.date_debut.isoformat()
objet_prepare["entry_date"] = objet.entry_date.strftime("%d/%m/%y à %H:%M")
objet_prepare["etud_nom"] = objet.etudiant.nomprenom
if objet.user_id != None:
user: User = User.query.get(objet.user_id)
objet_prepare["saisie_par"] = user.get_nomprenom()
else:
objet_prepare["saisie_par"] = "Inconnu"
return objet_prepare
@bp.route("/SignalAssiduiteDifferee") @bp.route("/SignalAssiduiteDifferee")
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
@ -1534,12 +1809,6 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s
# prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre # prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre
selected = "" if moduleimpl_id is not None else "selected" selected = "" if moduleimpl_id is not None else "selected"
# Vérification que le moduleimpl_id passé en paramètre est bien un entier
try:
moduleimpl_id = int(moduleimpl_id)
except (ValueError, TypeError):
moduleimpl_id = None
modules: list[dict[str, str | int]] = [] modules: list[dict[str, str | int]] = []
# Récupération de l'id et d'un nom lisible pour chaque moduleimpl # Récupération de l'id et d'un nom lisible pour chaque moduleimpl
for modimpl in modimpls_list: for modimpl in modimpls_list:

View File

@ -105,6 +105,7 @@ from app.scodoc import sco_synchro_etuds
from app.scodoc import sco_trombino from app.scodoc import sco_trombino
from app.scodoc import sco_trombino_tours from app.scodoc import sco_trombino_tours
from app.scodoc import sco_up_to_date from app.scodoc import sco_up_to_date
from app.tables import list_etuds
def sco_publish(route, function, permission, methods=["GET"]): def sco_publish(route, function, permission, methods=["GET"]):
@ -2071,6 +2072,33 @@ def check_group_apogee(group_id, etat=None, fix=False, fixmail=False):
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
@bp.route("/export_etudiants_courants")
@scodoc
@permission_required(Permission.ScoView)
def export_etudiants_courants():
"""Table export de tous les étudiants des formsemestres en cours."""
fmt = request.args.get("fmt", "html")
departement = Departement.query.get(g.scodoc_dept_id)
if not departement:
raise ScoValueError("département invalide")
formsemestres = FormSemestre.get_dept_formsemestres_courants(departement)
table = list_etuds.table_etudiants_courants(formsemestres)
if fmt.startswith("xls"):
return scu.send_file(
table.excel(),
f"""{formsemestres[0].departement.acronym}-etudiants-{
datetime.datetime.now().strftime("%Y-%m-%dT%Hh%M")}""",
scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
elif fmt == "html":
return render_template(
"scolar/export_etudiants_courants.j2", sco=ScoData(), table=table
)
else:
raise ScoValueError("invalid fmt value")
@bp.route("/form_students_import_excel", methods=["GET", "POST"]) @bp.route("/form_students_import_excel", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@ -2361,35 +2389,57 @@ def form_students_import_infos_admissions(formsemestre_id=None):
@scodoc @scodoc
@permission_required(Permission.EtudChangeAdr) @permission_required(Permission.EtudChangeAdr)
@scodoc7func @scodoc7func
def formsemestre_import_etud_admission(formsemestre_id, import_email=True): def formsemestre_import_etud_admission(
"""Ré-importe donnees admissions par synchro Portail Apogée""" formsemestre_id=None, import_email=True, tous_courants=False
( ):
no_nip, """Ré-importe donnees admissions par synchro Portail Apogée.
unknowns, Si tous_courants, le fait pour tous les formsemestres courants du département
changed_mails, """
) = sco_synchro_etuds.formsemestre_import_etud_admission( if tous_courants:
formsemestre_id, import_identite=True, import_email=import_email departement = Departement.query.get(g.scodoc_dept_id)
) formsemestres = FormSemestre.get_dept_formsemestres_courants(departement)
H = [ else:
html_sco_header.html_sem_header("Ré-import données admission"), formsemestres = [FormSemestre.get_formsemestre(formsemestre_id)]
"<h3>Opération effectuée</h3>",
] diag_by_sem = {}
if no_nip: for formsemestre in formsemestres:
H.append("<p>Attention: étudiants sans NIP: " + str(no_nip) + "</p>") (
if unknowns: etuds_no_nip,
H.append( etuds_unknown,
"<p>Attention: étudiants inconnus du portail: codes NIP=" changed_mails,
+ str(unknowns) ) = sco_synchro_etuds.formsemestre_import_etud_admission(
+ "</p>" formsemestre.id, import_identite=True, import_email=import_email
) )
if changed_mails: diag = ""
H.append("<h3>Adresses mails modifiées:</h3><ul>") if etuds_no_nip:
for etud, old_mail in changed_mails: diag += f"""<p>Attention: étudiants sans NIP:
H.append( {', '.join([e.html_link_fiche() for e in etuds_no_nip])}
f"""<li>{etud.nom}: <tt>{old_mail}</tt> devient <tt>{etud.email}</tt></li>""" </p>"""
)
H.append("</ul>") if etuds_unknown:
return "\n".join(H) + html_sco_header.sco_footer() diag += f"""<p>Attention: étudiants inconnus du portail:
{', '.join([(e.html_link_fiche() + ' (nip= ' + e.code_nip + ')')
for e in etuds_unknown])}
</p>"""
if changed_mails:
diag += """<p>Adresses mails modifiées:</p><ul>"""
for etud, old_mail in changed_mails:
diag += f"""<li>{etud.nom}: <tt>{old_mail}</tt> devient <tt>{etud.email}</tt></li>"""
diag += "</ul>"
diag_by_sem[formsemestre.id] = diag
return f"""
{ html_sco_header.html_sem_header("Ré-import données admission") }
<h3>Opération effectuée</h3>
<p>Sur le(s) semestres(s):</p>
<ul>
<li>
{ '</li><li>'.join( [(s.html_link_status() + diag_by_sem[s.id]) for s in formsemestres ]) }
</li>
</ul>
{ html_sco_header.sco_footer() }
"""
sco_publish( sco_publish(

View File

@ -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 += [
@ -690,10 +701,12 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
log(f"sco_users: editing {user_name} by {current_user.user_name}") log(f"sco_users: editing {user_name} by {current_user.user_name}")
log(f"sco_users: previous_values={initvalues}") log(f"sco_users: previous_values={initvalues}")
log(f"sco_users: new_values={vals}") log(f"sco_users: new_values={vals}")
sco_users.user_edit(user_name, vals)
flash(f"Utilisateur {user_name} modifié")
else: else:
sco_users.user_edit(user_name, {"roles_string": vals["roles_string"]}) vals = {"roles_string": vals["roles_string"]}
the_user.from_dict(vals)
db.session.add(the_user)
db.session.commit()
flash(f"Utilisateur {user_name} modifié")
return flask.redirect( return flask.redirect(
url_for( url_for(
"users.user_info_page", "users.user_info_page",
@ -749,7 +762,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
log( log(
f"""sco_users: new_user {vals["user_name"]} by {current_user.user_name}""" f"""sco_users: new_user {vals["user_name"]} by {current_user.user_name}"""
) )
the_user = User() the_user = User(user_name=user_name)
the_user.from_dict(vals, new_user=True) the_user.from_dict(vals, new_user=True)
db.session.add(the_user) db.session.add(the_user)
db.session.commit() db.session.commit()
@ -916,11 +929,12 @@ def user_info_page(user_name=None):
return render_template( return render_template(
"auth/user_info_page.j2", "auth/user_info_page.j2",
user=user,
title=f"Utilisateur {user.user_name}",
Permission=Permission,
dept=dept, dept=dept,
Permission=Permission,
ScoDocSiteConfig=ScoDocSiteConfig,
session_info=session_info, session_info=session_info,
title=f"Utilisateur {user.user_name}",
user=user,
) )

View File

@ -1,4 +1,5 @@
[pytest] [pytest]
norecursedirs = .git app/static
markers = markers =
slow: marks tests as slow (deselect with '-m "not slow"') slow: marks tests as slow (deselect with '-m "not slow"')
apo apo
@ -11,4 +12,4 @@ markers =
filterwarnings = filterwarnings =
ignore:.*json.*:DeprecationWarning ignore:.*json.*:DeprecationWarning
# en attendant mise à jour de Flask-JSON # en attendant mise à jour de Flask-JSON

View File

@ -88,7 +88,7 @@ python-editor==1.0.4
pytz==2023.3.post1 pytz==2023.3.post1
PyYAML==6.0.1 PyYAML==6.0.1
redis==5.0.1 redis==5.0.1
reportlab==4.0.5 reportlab==4.0.7
requests==2.31.0 requests==2.31.0
rq==1.15.1 rq==1.15.1
six==1.16.0 six==1.16.0

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.60" SCOVERSION = "9.6.64"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -28,10 +28,12 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
API_USER_ADMIN, API_USER_ADMIN,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
DEPT_ACRONYM,
GET,
POST_JSON, POST_JSON,
api_headers,
get_auth_headers, get_auth_headers,
) )
from tests.api.setup_test_api import api_headers # pylint: disable=unused-import
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
BULLETIN_ETUDIANT_FIELDS, BULLETIN_ETUDIANT_FIELDS,
BULLETIN_FIELDS, BULLETIN_FIELDS,
@ -923,3 +925,85 @@ def test_etudiant_groups(api_headers):
group = groups[0] group = groups[0]
fields_ok = verify_fields(group, fields) fields_ok = verify_fields(group, fields)
assert fields_ok is True assert fields_ok is True
def test_etudiant_create(api_headers):
"""/etudiant/create"""
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
args = {
"prenom": "Carl Philipp Emanuel",
"nom": "Bach",
"dept": DEPT_ACRONYM,
"civilite": "M",
"admission": {
"commentaire": "test",
"annee_bac": 2024,
},
"adresses": [
{
"villedomicile": "Santa Teresa",
"emailperso": "XXX@2666.mx",
}
],
}
etud = POST_JSON(
"/etudiant/create",
args,
headers=admin_header,
)
assert etud["nom"] == args["nom"].upper()
assert etud["admission"]["commentaire"] == args["admission"]["commentaire"]
assert etud["admission"]["annee_bac"] == args["admission"]["annee_bac"]
assert len(etud["adresses"]) == 1
assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"]
assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"]
etudid = etud["id"]
# On recommence avec une nouvelle requête:
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
assert etud["nom"] == args["nom"].upper()
assert etud["admission"]["commentaire"] == args["admission"]["commentaire"]
assert etud["admission"]["annee_bac"] == args["admission"]["annee_bac"]
assert len(etud["adresses"]) == 1
assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"]
assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"]
# Edition
etud = POST_JSON(
f"/etudiant/etudid/{etudid}/edit",
{
"civilite": "F",
"boursier": "N",
},
headers=admin_header,
)
assert etud["civilite"] == "F"
assert not etud["boursier"]
assert etud["nom"] == args["nom"].upper()
assert etud["admission"]["commentaire"] == args["admission"]["commentaire"]
assert etud["admission"]["annee_bac"] == args["admission"]["annee_bac"]
assert len(etud["adresses"]) == 1
assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"]
assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"]
etud = POST_JSON(
f"/etudiant/etudid/{etudid}/edit",
{
"adresses": [
{
"villedomicile": "Barcelona",
},
],
},
headers=admin_header,
)
assert etud["adresses"][0]["villedomicile"] == "Barcelona"
etud = POST_JSON(
f"/etudiant/etudid/{etudid}/edit",
{
"admission": {
"commentaire": "un nouveau commentaire",
},
"boursier": "O", # "oui", should be True
},
headers=admin_header,
)
assert etud["admission"]["commentaire"] == "un nouveau commentaire"
assert etud["boursier"]

View File

@ -90,6 +90,7 @@ def test_formsemestre_partition(api_headers):
) )
assert isinstance(group_r, dict) assert isinstance(group_r, dict)
assert group_r["group_name"] == group_d["group_name"] assert group_r["group_name"] == group_d["group_name"]
assert group_r["edt_id"] is None
# --- Liste groupes de la partition # --- Liste groupes de la partition
partition = GET(f"/partition/{partition_r['id']}", headers=headers) partition = GET(f"/partition/{partition_r['id']}", headers=headers)
assert isinstance(partition, dict) assert isinstance(partition, dict)
@ -99,6 +100,26 @@ def test_formsemestre_partition(api_headers):
group = partition["groups"][str(group_r["id"])] # nb: str car clés json en string group = partition["groups"][str(group_r["id"])] # nb: str car clés json en string
assert group["group_name"] == group_d["group_name"] assert group["group_name"] == group_d["group_name"]
# --- Ajout d'un groupe avec edt_id
group_d = {"group_name": "extra", "edt_id": "GEDT"}
group_r = POST_JSON(
f"/partition/{partition_r['id']}/group/create",
group_d,
headers=headers,
)
assert group_r["edt_id"] == "GEDT"
# Edit edt_id
group_r = POST_JSON(
f"/group/{group_r['id']}/edit",
{"edt_id": "GEDT2"},
headers=headers,
)
assert group_r["edt_id"] == "GEDT2"
partition = GET(f"/partition/{partition_r['id']}", headers=headers)
group = partition["groups"][str(group_r["id"])] # nb: str car clés json en string
assert group["group_name"] == group_d["group_name"]
assert group["edt_id"] == "GEDT2"
# Place un étudiant dans le groupe # Place un étudiant dans le groupe
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=headers)[0] etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=headers)[0]
repl = POST_JSON(f"/group/{group['id']}/set_etudiant/{etud['id']}", headers=headers) repl = POST_JSON(f"/group/{group['id']}/set_etudiant/{etud['id']}", headers=headers)

View File

@ -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):

View File

@ -47,6 +47,7 @@ def test_identite(test_client):
assert e.prenom == "PRENOM" assert e.prenom == "PRENOM"
assert e.prenom_etat_civil == "PRENOM_ETAT_CIVIL" assert e.prenom_etat_civil == "PRENOM_ETAT_CIVIL"
assert e.dept_naissance == "dept_naissance" assert e.dept_naissance == "dept_naissance"
assert e.etat_civil == "PRENOM_ETAT_CIVIL NOM"
# #
admission_id = e.admission_id admission_id = e.admission_id
admission = db.session.get(Admission, admission_id) admission = db.session.get(Admission, admission_id)
@ -81,32 +82,32 @@ def test_etat_civil(test_client):
dept = Departement.query.first() dept = Departement.query.first()
args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id} args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id}
# Homme # Homme
e = Identite(**args) e = Identite.create_etud(**args)
db.session.add(e)
db.session.flush() db.session.flush()
assert e.civilite_etat_civil_str == "M." assert e.civilite_etat_civil_str == "M."
assert e.e == "" assert e.e == ""
assert e.etat_civil == "M. PRÉNOM NOM"
# Femme # Femme
e = Identite(**args | {"civilite": "F"}) e = Identite.create_etud(**args | {"civilite": "F"})
db.session.add(e)
db.session.flush() db.session.flush()
assert e.civilite_etat_civil_str == "Mme" assert e.civilite_etat_civil_str == "Mme"
assert e.e == "e" assert e.e == "e"
assert e.etat_civil == "Mme PRÉNOM NOM"
# Homme devenu femme # Homme devenu femme
e = Identite(**(args | {"civilite_etat_civil": "F"})) e = Identite.create_etud(**(args | {"civilite_etat_civil": "F"}))
db.session.add(e)
db.session.flush() db.session.flush()
assert e.civilite_etat_civil_str == "Mme" assert e.civilite_etat_civil_str == "Mme"
assert e.civilite_str == "M." assert e.civilite_str == "M."
assert e.e == "" assert e.e == ""
assert e.etat_civil == "Mme PRÉNOM NOM"
# Femme devenue neutre # Femme devenue neutre
e = Identite(**(args | {"civilite": "X", "civilite_etat_civil": "F"})) e = Identite.create_etud(**(args | {"civilite": "X", "civilite_etat_civil": "F"}))
db.session.add(e)
db.session.flush() db.session.flush()
assert e.civilite_etat_civil_str == "Mme" assert e.civilite_etat_civil_str == "Mme"
assert e.civilite_str == "" assert e.civilite_str == ""
assert e.e == "(e)" assert e.e == "(e)"
assert e.prenom_etat_civil is None assert e.prenom_etat_civil is None
assert e.etat_civil == "Mme PRÉNOM NOM"
# La version dict # La version dict
e_d = e.to_dict_scodoc7() e_d = e.to_dict_scodoc7()
assert e_d["civilite"] == "X" assert e_d["civilite"] == "X"
@ -119,7 +120,7 @@ def test_etud_legacy(test_client):
dept = Departement.query.first() dept = Departement.query.first()
args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id} args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id}
# Prénom état civil # Prénom état civil
e = Identite(**(args)) e = Identite.create_etud(**(args))
db.session.add(e) db.session.add(e)
db.session.flush() db.session.flush()
e_dict = e.to_dict_bul() e_dict = e.to_dict_bul()

View File

@ -123,3 +123,30 @@ 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 = {
"prenom": "No Totoro",
"edt_id": "totorito",
"cas_allow_login": 1, # boolean
"irrelevant": "..", # intentionnellement en dehors des attributs
}
u = User(user_name="Tonari")
u.from_dict(args)
db.session.add(u)
db.session.commit()
db.session.refresh(u)
assert u.edt_id == "totorito"
assert u.nom == ""
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()