forked from ScoDoc/ScoDoc
Compare commits
19 Commits
c0253bd05d
...
5cd495e33e
Author | SHA1 | Date | |
---|---|---|---|
5cd495e33e | |||
a5e5ad6248 | |||
7cda427cac | |||
a0316c22e7 | |||
2d3490e7fa | |||
dcea12f6fc | |||
568c8681ba | |||
|
cbb11d0e8e | ||
|
7a80ec3ce5 | ||
|
b13e751e1a | ||
60109bb513 | |||
e634b50d56 | |||
2377918b54 | |||
532fb3e701 | |||
457a9ddf51 | |||
ea1a03a654 | |||
e41879a1e1 | |||
4d3cbf7e75 | |||
939371cff9 |
@ -7,7 +7,7 @@
|
||||
"""
|
||||
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).
|
||||
"""
|
||||
from datetime import datetime
|
||||
@ -271,23 +271,11 @@ def dept_formsemestres_courants(acronym: str):
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
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,
|
||||
)
|
||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
||||
return [
|
||||
d.to_dict_api()
|
||||
for d in formsemestres.order_by(
|
||||
FormSemestre.date_debut.desc(),
|
||||
FormSemestre.modalite,
|
||||
FormSemestre.semestre_id,
|
||||
FormSemestre.titre,
|
||||
formsemestre.to_dict_api()
|
||||
for formsemestre in FormSemestre.get_dept_formsemestres_courants(
|
||||
dept, date_courante
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -18,20 +18,24 @@ from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.but import bulletin_but_court
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
Admission,
|
||||
Adresse,
|
||||
Departement,
|
||||
FormSemestreInscription,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
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_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)
|
||||
|
||||
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
|
||||
|
@ -646,8 +646,8 @@ def justif_import(justif_id: int = None):
|
||||
return json_error(404, err.args[0])
|
||||
|
||||
|
||||
@bp.route("/justificatif/<int:justif_id>/export/<filename>", methods=["POST"])
|
||||
@api_web_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=["GET", "POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.AbsChange)
|
||||
|
@ -303,15 +303,19 @@ def group_create(partition_id: int): # partition-group-create
|
||||
return json_error(403, "partition non editable")
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
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.commit()
|
||||
log(f"created group {group}")
|
||||
@ -369,16 +373,22 @@ def group_edit(group_id: int):
|
||||
return json_error(403, "partition non editable")
|
||||
if not group.partition.formsemestre.can_change_groups():
|
||||
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 not None:
|
||||
group_name = group_name.strip()
|
||||
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if "group_name" in args:
|
||||
if not isinstance(args["group_name"], str):
|
||||
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")
|
||||
group.group_name = group_name
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"modified {group}")
|
||||
|
||||
group.from_dict(args)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"modified {group}")
|
||||
|
||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
return group.to_dict(with_partition=True)
|
||||
|
@ -7,7 +7,7 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -85,6 +85,20 @@ def users_info_query():
|
||||
return [user.to_dict() for user in query]
|
||||
|
||||
|
||||
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
||||
"Vrai si on peut"
|
||||
if "cas_id" in args and not current_user.has_permission(
|
||||
Permission.UsersChangeCASId
|
||||
):
|
||||
return False, "non autorise a changer cas_id"
|
||||
|
||||
if not current_user.is_administrator():
|
||||
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
|
||||
if field in args:
|
||||
return False, f"non autorise a changer {field}"
|
||||
return True, ""
|
||||
|
||||
|
||||
@bp.route("/user/create", methods=["POST"])
|
||||
@api_web_bp.route("/user/create", methods=["POST"])
|
||||
@login_required
|
||||
@ -95,21 +109,22 @@ def user_create():
|
||||
"""Création d'un utilisateur
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"user_name": str,
|
||||
"active":bool (default True),
|
||||
"dept": str or null,
|
||||
"nom": str,
|
||||
"prenom": str,
|
||||
"active":bool (default True)
|
||||
"user_name": str,
|
||||
...
|
||||
}
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = data.get("user_name")
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = args.get("user_name")
|
||||
if not user_name:
|
||||
return json_error(404, "empty user_name")
|
||||
user = User.query.filter_by(user_name=user_name).first()
|
||||
if user:
|
||||
return json_error(404, f"user_create: user {user} already exists\n")
|
||||
dept = data.get("dept")
|
||||
dept = args.get("dept")
|
||||
if dept == "@all":
|
||||
dept = None
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
||||
@ -119,10 +134,12 @@ def user_create():
|
||||
Departement.query.filter_by(acronym=dept).first() is None
|
||||
):
|
||||
return json_error(404, "user_create: departement inexistant")
|
||||
nom = data.get("nom")
|
||||
prenom = data.get("prenom")
|
||||
active = scu.to_bool(data.get("active", True))
|
||||
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
|
||||
args["dept"] = dept
|
||||
ok, msg = _is_allowed_user_edit(args)
|
||||
if not ok:
|
||||
return json_error(403, f"user_create: {msg}")
|
||||
user = User(user_name=user_name)
|
||||
user.from_dict(args, new_user=True)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user.to_dict()
|
||||
@ -142,13 +159,14 @@ def user_edit(uid: int):
|
||||
"nom": str,
|
||||
"prenom": str,
|
||||
"active":bool
|
||||
...
|
||||
}
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user: User = User.query.get_or_404(uid)
|
||||
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
||||
orig_dept = user.dept
|
||||
dest_dept = data.get("dept", False)
|
||||
dest_dept = args.get("dept", False)
|
||||
if dest_dept is not False:
|
||||
if dest_dept == "@all":
|
||||
dest_dept = None
|
||||
@ -164,10 +182,11 @@ def user_edit(uid: int):
|
||||
return json_error(404, "user_edit: departement inexistant")
|
||||
user.dept = dest_dept
|
||||
|
||||
user.nom = data.get("nom", user.nom)
|
||||
user.prenom = data.get("prenom", user.prenom)
|
||||
user.active = scu.to_bool(data.get("active", user.active))
|
||||
ok, msg = _is_allowed_user_edit(args)
|
||||
if not ok:
|
||||
return json_error(403, f"user_edit: {msg}")
|
||||
|
||||
user.from_dict(args)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user.to_dict()
|
||||
|
@ -12,7 +12,6 @@ from typing import Optional
|
||||
|
||||
import cracklib # pylint: disable=import-error
|
||||
|
||||
import flask
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
|
||||
@ -21,14 +20,13 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import jwt
|
||||
|
||||
from app import db, email, log, login
|
||||
from app.models import Departement
|
||||
from app.models import Departement, ScoDocModel
|
||||
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||
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@\\\-_\.]+$")
|
||||
|
||||
@ -53,13 +51,14 @@ def is_valid_password(cleartxt) -> bool:
|
||||
def invalid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is invalid"
|
||||
return (
|
||||
(len(user_name) < 2)
|
||||
not user_name
|
||||
or (len(user_name) < 2)
|
||||
or (len(user_name) >= USERNAME_STR_LEN)
|
||||
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"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -116,12 +115,17 @@ class User(UserMixin, db.Model):
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"user_name:str is mandatory"
|
||||
self.roles = []
|
||||
self.user_roles = []
|
||||
# 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']}")
|
||||
super(User, self).__init__(**kwargs)
|
||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
||||
super().__init__(**kwargs)
|
||||
# Ajoute roles:
|
||||
if (
|
||||
not self.roles
|
||||
@ -251,12 +255,13 @@ class User(UserMixin, db.Model):
|
||||
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||
if self.cas_last_login
|
||||
else None,
|
||||
"edt_id": self.edt_id,
|
||||
"status_txt": "actif" if self.active else "fermé",
|
||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||
"nom": (self.nom or ""), # sco8
|
||||
"prenom": (self.prenom or ""), # sco8
|
||||
"nom": self.nom or "",
|
||||
"prenom": self.prenom or "",
|
||||
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
||||
"user_name": self.user_name, # sco8
|
||||
"user_name": self.user_name,
|
||||
# Les champs calculés:
|
||||
"nom_fmt": self.get_nom_fmt(),
|
||||
"prenom_fmt": self.get_prenom_fmt(),
|
||||
@ -270,37 +275,50 @@ class User(UserMixin, db.Model):
|
||||
data["email_institutionnel"] = self.email_institutionnel or ""
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No other side effect.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
Convert boolean values to bools.
|
||||
"""
|
||||
args_dict = args
|
||||
# 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):
|
||||
"""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 "user_name" in data:
|
||||
# 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"]
|
||||
if "password" in data:
|
||||
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, ..."
|
||||
if "roles_string" in data:
|
||||
self.user_roles = []
|
||||
@ -309,6 +327,8 @@ class User(UserMixin, db.Model):
|
||||
role, dept = UserRole.role_dept_from_string(r_d)
|
||||
self.add_role(role, dept)
|
||||
|
||||
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
|
||||
|
||||
# Set cas_id using regexp if configured:
|
||||
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
||||
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
|
||||
e.g. Dupont Pierre (dupont)
|
||||
"""
|
||||
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
|
||||
nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
||||
|
||||
@staticmethod
|
||||
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||
@ -460,29 +480,29 @@ class User(UserMixin, db.Model):
|
||||
def get_nom_fmt(self):
|
||||
"""Nom formaté: "Martin" """
|
||||
if self.nom:
|
||||
return sco_etud.format_nom(self.nom, uppercase=False)
|
||||
return scu.format_nom(self.nom, uppercase=False)
|
||||
else:
|
||||
return self.user_name
|
||||
|
||||
def get_prenom_fmt(self):
|
||||
"""Prénom formaté (minuscule capitalisées)"""
|
||||
return sco_etud.format_prenom(self.prenom)
|
||||
return scu.format_prenom(self.prenom)
|
||||
|
||||
def get_nomprenom(self):
|
||||
"""Nom capitalisé suivi de l'initiale du prénom:
|
||||
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()
|
||||
|
||||
def get_prenomnom(self):
|
||||
"""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()
|
||||
|
||||
def get_nomcomplet(self):
|
||||
"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)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from flask import Blueprint
|
||||
from app.scodoc import sco_etud
|
||||
from app.auth.models import User
|
||||
from app.models import Departement
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
bp = Blueprint("entreprises", __name__)
|
||||
|
||||
@ -15,12 +16,12 @@ SIRET_PROVISOIRE_START = "xx"
|
||||
|
||||
@bp.app_template_filter()
|
||||
def format_prenom(s):
|
||||
return sco_etud.format_prenom(s)
|
||||
return scu.format_prenom(s)
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
def format_nom(s):
|
||||
return sco_etud.format_nom(s)
|
||||
return scu.format_nom(s)
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
|
@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
elif request.method == "GET":
|
||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||
form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
|
||||
scu.format_prenom(etudiant.prenom)}"""
|
||||
form.etudid.data = etudiant.id
|
||||
form.type_offre.data = stage_apprentissage.type_offre
|
||||
form.date_debut.data = stage_apprentissage.date_debut
|
||||
@ -1699,7 +1699,7 @@ def json_etudiants():
|
||||
list = []
|
||||
for etudiant in etudiants:
|
||||
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:
|
||||
content = {
|
||||
"id": f"{etudiant.id}",
|
||||
|
@ -53,8 +53,9 @@ class ScoDocModel:
|
||||
@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.
|
||||
By default, excluded == { 'id' }"""
|
||||
excluded = {"id"} if excluded is None else set()
|
||||
Add 'id' to excluded."""
|
||||
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)
|
||||
my_attributes = [
|
||||
a
|
||||
@ -70,7 +71,7 @@ class ScoDocModel:
|
||||
|
||||
@classmethod
|
||||
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.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
@ -78,9 +79,11 @@ class ScoDocModel:
|
||||
# virtual, by default, do nothing
|
||||
return args
|
||||
|
||||
def from_dict(self, args: dict):
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None):
|
||||
"Update object's fields given in dict. Add to session but don't commit."
|
||||
args_dict = self.convert_dict_fields(self.filter_model_attributes(args))
|
||||
args_dict = self.convert_dict_fields(
|
||||
self.filter_model_attributes(args, excluded=excluded)
|
||||
)
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
@ -130,7 +133,6 @@ from app.models.notes import (
|
||||
NotesNotesLog,
|
||||
)
|
||||
from app.models.validations import (
|
||||
ScolarEvent,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
)
|
||||
@ -149,3 +151,4 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
|
@ -3,8 +3,8 @@
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from app import db, log
|
||||
from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription
|
||||
from app import db, log, g
|
||||
from app.models import ModuleImpl, Module, Scolog, FormSemestre, FormSemestreInscription
|
||||
from app.models.etudiants import Identite
|
||||
from app.auth.models import User
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
@ -334,6 +405,39 @@ class Justificatif(db.Model):
|
||||
)
|
||||
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(
|
||||
date_debut: datetime,
|
||||
|
@ -15,7 +15,8 @@ from sqlalchemy import desc, text
|
||||
|
||||
from app import db, log
|
||||
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.sco_bac import Baccalaureat
|
||||
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||
@ -170,9 +171,13 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
return f"""<a class="stdlink" href="{self.url_fiche()}">{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
|
||||
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)
|
||||
|
||||
@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.
|
||||
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)
|
||||
"""
|
||||
etud: Identite = super(cls, cls).create_from_dict(data)
|
||||
if (data.get("admission_id", None) is None) and (
|
||||
data.get("admission", None) is None
|
||||
):
|
||||
if not "dept_id" in args:
|
||||
if "dept" in args:
|
||||
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.adresses.append(Adresse(typeadresse="domicile"))
|
||||
db.session.flush()
|
||||
|
||||
event = ScolarEvent(etud=etud, event_type="CREATION")
|
||||
db.session.add(event)
|
||||
log(f"Identite.create {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
|
||||
def civilite_str(self) -> str:
|
||||
"""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:
|
||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
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
|
||||
prenom = self.prenom_str
|
||||
if reverse:
|
||||
fields = (nom, prenom)
|
||||
else:
|
||||
fields = (self.civilite_str, prenom, nom)
|
||||
return " ".join([x for x in fields if x])
|
||||
return f"{nom} {prenom}".strip()
|
||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
||||
|
||||
@property
|
||||
def prenom_str(self):
|
||||
@ -282,12 +306,10 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
|
||||
@property
|
||||
def etat_civil(self) -> str:
|
||||
"M. Prénom NOM, utilisant les données état civil si présentes, usuelles sinon."
|
||||
if self.prenom_etat_civil:
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil]
|
||||
return f"{civ} {self.prenom_etat_civil} {self.nom}"
|
||||
else:
|
||||
return self.nomprenom
|
||||
"M. PRÉNOM NOM, utilisant les données état civil si présentes, usuelles sinon."
|
||||
return f"""{self.civilite_etat_civil_str} {
|
||||
self.prenom_etat_civil or self.prenom or ''} {
|
||||
self.nom or ''}""".strip()
|
||||
|
||||
@property
|
||||
def nom_short(self):
|
||||
@ -321,8 +343,6 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""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.
|
||||
"""
|
||||
# Les champs qui sont toujours stockés en majuscules:
|
||||
@ -341,8 +361,6 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
"code_ine",
|
||||
}
|
||||
args_dict = {}
|
||||
if not "dept_id" in args:
|
||||
args["dept_id"] = g.scodoc_dept_id
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||
# compat scodoc7 (mauvaise idée de l'époque)
|
||||
@ -355,7 +373,7 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
elif key == "civilite_etat_civil":
|
||||
value = input_civilite_etat_civil(value)
|
||||
elif key == "boursier":
|
||||
value = bool(value)
|
||||
value = scu.to_bool(value)
|
||||
elif key == "date_naissance":
|
||||
value = ndb.DateDMYtoISO(value)
|
||||
args_dict[key] = value
|
||||
|
@ -133,7 +133,7 @@ class ScolarNews(db.Model):
|
||||
return query.order_by(cls.date.desc()).limit(n).all()
|
||||
|
||||
@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
|
||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||
à 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).
|
||||
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:
|
||||
last_news = (
|
||||
cls.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
dept_id=dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
@ -163,7 +164,7 @@ class ScolarNews(db.Model):
|
||||
return
|
||||
|
||||
news = ScolarNews(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
dept_id=dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
|
@ -30,6 +30,7 @@ from app.models.but_refcomp import (
|
||||
parcours_formsemestre,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.formations import Formation
|
||||
@ -521,7 +522,7 @@ class FormSemestre(db.Model):
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
jour_pivot_annee=1,
|
||||
jour_pivot_periode=1,
|
||||
):
|
||||
) -> tuple[int, int]:
|
||||
"""Calcule la session associée à un formsemestre commençant en date_debut
|
||||
sous la forme (année, période)
|
||||
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(),
|
||||
)
|
||||
|
||||
@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]:
|
||||
"Liste des vdis"
|
||||
# was read_formsemestre_etapes
|
||||
|
@ -11,14 +11,14 @@ from operator import attrgetter
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
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.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
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"""
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
||||
@ -204,7 +204,7 @@ class Partition(db.Model):
|
||||
return group
|
||||
|
||||
|
||||
class GroupDescr(db.Model):
|
||||
class GroupDescr(db.Model, ScoDocModel):
|
||||
"""Description d'un groupe d'une partition"""
|
||||
|
||||
__tablename__ = "group_descr"
|
||||
|
48
app/models/scolar_event.py
Normal file
48
app/models/scolar_event.py
Normal 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})"
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Notes, décisions de jury, évènements scolaires
|
||||
"""Notes, décisions de jury
|
||||
"""
|
||||
|
||||
from app import db
|
||||
@ -218,47 +218,3 @@ class ScolarAutorisationInscription(db.Model):
|
||||
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||
)
|
||||
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})"
|
||||
|
@ -149,21 +149,49 @@ def index_html(showcodes=0, showsemtable=0):
|
||||
</p>"""
|
||||
)
|
||||
#
|
||||
if current_user.has_permission(Permission.EtudInscrit):
|
||||
H.append(
|
||||
"""<hr>
|
||||
H.append(
|
||||
"""<hr>
|
||||
<h3>Gestion des étudiants</h3>
|
||||
<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><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
|
||||
(ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
|
||||
<li><a class="stdlink" href="{
|
||||
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 : utilisez plutôt le lien dans
|
||||
le tableau de bord semestre si vous souhaitez inscrire les
|
||||
étudiants importés à un semestre)
|
||||
</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):
|
||||
H.append(
|
||||
|
@ -45,6 +45,12 @@ from app.models.etudiants import (
|
||||
pivot_year,
|
||||
)
|
||||
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
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
from app.scodoc import safehtml
|
||||
@ -102,60 +108,6 @@ def force_uppercase(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:
|
||||
"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"]:
|
||||
@ -657,16 +609,6 @@ def create_etud(cnx, args: dict = None):
|
||||
db.session.commit()
|
||||
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
|
||||
logdb(
|
||||
cnx,
|
||||
@ -674,16 +616,18 @@ def create_etud(cnx, args: dict = None):
|
||||
etudid=etudid,
|
||||
msg="creation initiale",
|
||||
)
|
||||
etud = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud])
|
||||
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud_dict])
|
||||
etud_dict["url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
||||
url=etud["url"],
|
||||
text=f"Nouvel étudiant {etud.html_link_fiche()}",
|
||||
url=etud_dict["url"],
|
||||
max_frequency=0,
|
||||
)
|
||||
return etud
|
||||
return etud_dict
|
||||
|
||||
|
||||
# ---------- "EVENTS"
|
||||
|
@ -40,6 +40,8 @@ from openpyxl.comments import Comment
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
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
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -218,6 +220,7 @@ class ScoExcelSheet:
|
||||
self.rows = [] # list of list of cells
|
||||
self.column_dimensions = {}
|
||||
self.row_dimensions = {}
|
||||
self.formulae = {}
|
||||
|
||||
def excel_make_composite_style(
|
||||
self,
|
||||
@ -363,6 +366,9 @@ class ScoExcelSheet:
|
||||
"""ajoute une ligne déjà construite à la feuille."""
|
||||
self.rows.append(row)
|
||||
|
||||
def set_formula(self, coord, formula):
|
||||
self.formulae[coord] = formula
|
||||
|
||||
def prepare(self):
|
||||
"""génére un flux décrivant la feuille.
|
||||
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)
|
||||
self.prepare()
|
||||
for coord, formula in self.formulae.items():
|
||||
self.ws[coord] = formula
|
||||
with NamedTemporaryFile() as tmp:
|
||||
self.wb.save(tmp.name)
|
||||
tmp.seek(0)
|
||||
@ -433,7 +441,12 @@ def excel_simple_table(
|
||||
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.
|
||||
E: evaluation (dict)
|
||||
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("E", 115.0 / 7) # notes
|
||||
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
|
||||
font_base = Font(name="Arial", size=12)
|
||||
@ -497,15 +517,32 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
|
||||
"font": font_blue,
|
||||
"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
|
||||
ws.append_single_cell_row(
|
||||
"Feuille saisie note (à enregistrer au format excel)", style_titres
|
||||
)
|
||||
# lignes d'instructions
|
||||
ws.append_single_cell_row(
|
||||
"Saisir les notes dans la colonne E (cases jaunes)", style_expl
|
||||
)
|
||||
if withnips:
|
||||
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)
|
||||
# Nom du semestre
|
||||
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
|
||||
ws.append_blank_row()
|
||||
# code et titres colonnes
|
||||
ws.append_row(
|
||||
[
|
||||
ws.make_cell("!%s" % evaluation.id, style_ro),
|
||||
ws.make_cell("Nom", style_titres),
|
||||
ws.make_cell("Prénom", 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("Remarque", style_titres),
|
||||
title_row = [
|
||||
ws.make_cell("!%s" % evaluation.id, style_ro),
|
||||
ws.make_cell("Nom", style_titres),
|
||||
ws.make_cell("Prénom", 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("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
|
||||
for line in lines:
|
||||
for line_number, line in enumerate(lines):
|
||||
st = style_nom
|
||||
if line[3] != "I":
|
||||
st = style_dem
|
||||
@ -543,17 +594,30 @@ def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, line
|
||||
try:
|
||||
val = float(line[5])
|
||||
except ValueError:
|
||||
val = line[5]
|
||||
ws.append_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 and line[5] == "":
|
||||
ws.set_formula(
|
||||
f"E{list_top + line_number}",
|
||||
f"=VLOOKUP(G{list_top + line_number},{input_range}, 2, FALSE)",
|
||||
)
|
||||
val = ""
|
||||
else:
|
||||
val = line[5]
|
||||
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
|
||||
ws.append_row([None, ws.make_cell("Code notes", style_titres)])
|
||||
|
@ -42,6 +42,7 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def form_search_etud(
|
||||
@ -271,7 +272,7 @@ def search_etud_by_name(term: str) -> list:
|
||||
data = [
|
||||
{
|
||||
"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"],
|
||||
}
|
||||
for x in r
|
||||
@ -290,7 +291,7 @@ def search_etud_by_name(term: str) -> list:
|
||||
|
||||
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"],
|
||||
}
|
||||
for x in r
|
||||
|
@ -39,7 +39,7 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
|
||||
from app.models.etudiants import Identite
|
||||
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
|
||||
from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
@ -222,10 +222,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT Im.id AS moduleimpl_inscription_id
|
||||
"""SELECT Im.id AS moduleimpl_inscription_id
|
||||
FROM notes_moduleimpl_inscription Im, notes_moduleimpl M
|
||||
WHERE Im.etudid=%(etudid)s
|
||||
and Im.moduleimpl_id = M.id
|
||||
and Im.moduleimpl_id = M.id
|
||||
and M.formsemestre_id = %(formsemestre_id)s
|
||||
""",
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id},
|
||||
@ -253,7 +253,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
||||
nbinscrits = len(inscrits)
|
||||
if nbinscrits == 0:
|
||||
log(
|
||||
f"""do_formsemestre_desinscription:
|
||||
f"""do_formsemestre_desinscription:
|
||||
suppression du semestre extérieur {formsemestre}"""
|
||||
)
|
||||
flash("Semestre exterieur supprimé")
|
||||
@ -436,7 +436,7 @@ def formsemestre_inscription_with_modules(
|
||||
if inscr is not None:
|
||||
H.append(
|
||||
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()}
|
||||
</p>
|
||||
<ul>
|
||||
@ -482,8 +482,8 @@ def formsemestre_inscription_with_modules(
|
||||
H.append("</ul>")
|
||||
H.append(
|
||||
f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
|
||||
multiple_ok=1,
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
|
||||
multiple_ok=1,
|
||||
group_ids=group_ids )
|
||||
}">Continuer quand même l'inscription</a>
|
||||
</p>"""
|
||||
@ -644,7 +644,7 @@ function chkbx_select(field_id, state) {
|
||||
"""
|
||||
<p>Voici la liste des modules du semestre choisi.</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.
|
||||
</p>
|
||||
<p>Attention: cette méthode ne devrait être utilisée que pour les modules
|
||||
|
@ -53,6 +53,7 @@ from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cursus
|
||||
from app.scodoc import sco_etud
|
||||
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.sco_exceptions import ScoException, AccessDenied, ScoValueError
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
@ -573,8 +574,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
etudid=str(e["etudid"]),
|
||||
civilite=etud["civilite_str"] or "",
|
||||
sexe=etud["civilite_str"] or "", # compat
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
nom=scu.format_nom(etud["nom"] or ""),
|
||||
prenom=scu.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
@ -599,8 +600,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
"etud",
|
||||
etudid=str(etud["etudid"]),
|
||||
sexe=etud["civilite_str"] or "",
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
nom=scu.format_nom(etud["nom"] or ""),
|
||||
prenom=scu.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
|
@ -101,7 +101,8 @@ def group_rename(group_id):
|
||||
"allow_null": True,
|
||||
"explanation": """optionnel : identifiant du groupe dans le logiciel
|
||||
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).""",
|
||||
},
|
||||
),
|
||||
|
@ -254,7 +254,7 @@ def import_users(users, force="") -> tuple[bool, list[str], int]:
|
||||
if import_ok:
|
||||
for u in created.values():
|
||||
# Création de l'utilisateur (via SQLAlchemy)
|
||||
user = User()
|
||||
user = User(user_name=u["user_name"])
|
||||
user.from_dict(u, new_user=True)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
@ -244,8 +244,8 @@ def feuille_preparation_jury(formsemestre_id):
|
||||
[
|
||||
etud.id,
|
||||
etud.civilite_str,
|
||||
sco_etud.format_nom(etud.nom),
|
||||
sco_etud.format_prenom(etud.prenom),
|
||||
scu.format_nom(etud.nom),
|
||||
scu.format_prenom(etud.prenom),
|
||||
etud.date_naissance,
|
||||
etud.admission.bac if etud.admission else "",
|
||||
etud.admission.specialite if etud.admission else "",
|
||||
|
@ -733,6 +733,8 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
<li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
|
||||
groups_infos.groups_query_args}"
|
||||
id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a>
|
||||
(<a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
|
||||
groups_infos.groups_query_args}&withnips=1">saisie par NIP</a>)
|
||||
</li>
|
||||
<li>ou <a class="stdlink" href="{url_for("notes.saisie_notes",
|
||||
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)
|
||||
|
||||
|
||||
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"""
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
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
|
||||
rows = []
|
||||
|
||||
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id)
|
||||
for e in etuds:
|
||||
etudid = e["etudid"]
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
|
||||
rows.append(
|
||||
[
|
||||
str(etudid),
|
||||
e["nom"].upper(),
|
||||
e["prenom"].lower().capitalize(),
|
||||
e["inscr"]["etat"],
|
||||
grc,
|
||||
e["val"],
|
||||
e["explanation"],
|
||||
]
|
||||
)
|
||||
row = [
|
||||
str(etudid),
|
||||
e["nom"].upper(),
|
||||
e["prenom"].lower().capitalize(),
|
||||
e["inscr"]["etat"],
|
||||
grc,
|
||||
e["val"],
|
||||
e["explanation"],
|
||||
]
|
||||
if withnips == 1:
|
||||
row.append(e["code_nip"])
|
||||
rows.append(row)
|
||||
|
||||
filename = f"notes_{eval_name}_{gr_title_filename}"
|
||||
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)
|
||||
|
||||
@ -1244,7 +1249,7 @@ def _form_saisie_notes(
|
||||
'<span class="%s">' % classdem
|
||||
+ e["civilite_str"]
|
||||
+ " "
|
||||
+ sco_etud.format_nomprenom(e, reverse=True)
|
||||
+ scu.format_nomprenom(e, reverse=True)
|
||||
+ "</span>"
|
||||
)
|
||||
|
||||
|
@ -793,21 +793,25 @@ def update_etape_formsemestre_inscription(ins, etud):
|
||||
|
||||
|
||||
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
|
||||
pour tous les étudiants du semestre.
|
||||
Si import_identite==True, recopie l'identité (nom/prenom/sexe/date_naissance)
|
||||
de chaque étudiant depuis 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)
|
||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
{"formsemestre_id": formsemestre_id}
|
||||
)
|
||||
log(f"formsemestre_import_etud_admission: {formsemestre_id} ({len(ins)} etuds)")
|
||||
no_nip = [] # liste d'etudids sans code NIP
|
||||
unknowns = [] # etudiants avec NIP mais inconnus du portail
|
||||
etuds_no_nip: list[Identite] = []
|
||||
etuds_unknown: list[Identite] = []
|
||||
changed_mails: list[tuple[Identite, str]] = [] # modification d'adresse mails
|
||||
|
||||
# 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)
|
||||
code_nip = etud.code_nip
|
||||
if not code_nip:
|
||||
no_nip.append(etudid)
|
||||
etuds_no_nip.append(etud)
|
||||
else:
|
||||
data_apo = apo_etuds.get(code_nip)
|
||||
if not data_apo:
|
||||
@ -865,7 +869,7 @@ def formsemestre_import_etud_admission(
|
||||
if adresse.email != data_apo["mail"]:
|
||||
changed_mails.append((etud, old_mail))
|
||||
else:
|
||||
unknowns.append(code_nip)
|
||||
etuds_unknown.append(etud)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])
|
||||
return no_nip, unknowns, changed_mails
|
||||
return etuds_no_nip, etuds_unknown, changed_mails
|
||||
|
@ -129,7 +129,7 @@ def trombino_html(groups_infos):
|
||||
H = [
|
||||
f"""<table style="padding-top: 10px; padding-bottom: 10px;">
|
||||
<tr>
|
||||
<td><span
|
||||
<td><span
|
||||
style="font-style: bold; font-size: 150%%; padding-right: 20px;"
|
||||
>{group_txt}</span></td>"""
|
||||
]
|
||||
@ -164,9 +164,9 @@ def trombino_html(groups_infos):
|
||||
H.append("</span>")
|
||||
H.append(
|
||||
'<span class="trombi_legend"><span class="trombi_prenom">'
|
||||
+ sco_etud.format_prenom(t["prenom"])
|
||||
+ scu.format_prenom(t["prenom"])
|
||||
+ '</span><span class="trombi_nom">'
|
||||
+ sco_etud.format_nom(t["nom"])
|
||||
+ scu.format_nom(t["nom"])
|
||||
+ (" <i>(dem.)</i>" if t["etat"] == "D" else "")
|
||||
)
|
||||
H.append("</span></span></span>")
|
||||
@ -175,10 +175,10 @@ def trombino_html(groups_infos):
|
||||
H.append("</div>")
|
||||
H.append(
|
||||
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>
|
||||
|
||||
<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>
|
||||
</div>"""
|
||||
)
|
||||
@ -202,9 +202,9 @@ def check_local_photos_availability(groups_infos, fmt=""):
|
||||
return (
|
||||
False,
|
||||
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>
|
||||
<p>Vous pouvez <a class="stdlink"
|
||||
<p>Vous pouvez <a class="stdlink"
|
||||
href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}"
|
||||
>exporter seulement les photos existantes</a>""",
|
||||
dest_url="trombino",
|
||||
@ -263,11 +263,11 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
|
||||
if not dialog_confirmed:
|
||||
return scu.confirm_dialog(
|
||||
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).
|
||||
</p>
|
||||
<p>(les photos sont normalement automatiquement copiées
|
||||
lors de leur première utilisation, l'usage de cette fonction
|
||||
<p>(les photos sont normalement automatiquement copiées
|
||||
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)
|
||||
</p>
|
||||
""",
|
||||
@ -349,7 +349,7 @@ def _trombino_pdf(groups_infos):
|
||||
[img],
|
||||
[
|
||||
Paragraph(
|
||||
SU(sco_etud.format_nomprenom(t)),
|
||||
SU(scu.format_nomprenom(t)),
|
||||
style_sheet["Normal"],
|
||||
)
|
||||
],
|
||||
@ -428,7 +428,7 @@ def _listeappel_photos_pdf(groups_infos):
|
||||
t = groups_infos.members[i]
|
||||
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
|
||||
txt = Paragraph(
|
||||
SU(sco_etud.format_nomprenom(t)),
|
||||
SU(scu.format_nomprenom(t)),
|
||||
style_sheet["Normal"],
|
||||
)
|
||||
if currow:
|
||||
|
@ -55,7 +55,7 @@ def trombino_doc(groups_infos):
|
||||
cell = table.rows[2 * li + 1].cells[co]
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
|
||||
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)
|
||||
|
||||
return scu.send_docx(document, filename)
|
||||
|
@ -196,9 +196,9 @@ def pdf_trombino_tours(
|
||||
Paragraph(
|
||||
SU(
|
||||
"<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
|
||||
+ "</font></para>"
|
||||
),
|
||||
@ -413,11 +413,7 @@ def pdf_feuille_releve_absences(
|
||||
for m in members:
|
||||
currow = [
|
||||
Paragraph(
|
||||
SU(
|
||||
sco_etud.format_nom(m["nom"])
|
||||
+ " "
|
||||
+ sco_etud.format_prenom(m["prenom"])
|
||||
),
|
||||
SU(scu.format_nom(m["nom"]) + " " + scu.format_prenom(m["prenom"])),
|
||||
StyleSheet["Normal"],
|
||||
)
|
||||
]
|
||||
|
@ -102,7 +102,7 @@ def index_html(
|
||||
<option value="">--Choisir--</option>
|
||||
{menu_roles}
|
||||
</select>
|
||||
|
||||
|
||||
</form>
|
||||
"""
|
||||
)
|
||||
@ -204,7 +204,7 @@ def list_users(
|
||||
"cas_allow_scodoc_login",
|
||||
"cas_last_login",
|
||||
]
|
||||
columns_ids.append("email_institutionnel")
|
||||
columns_ids += ["email_institutionnel", "edt_id"]
|
||||
|
||||
title = "Utilisateurs définis dans ScoDoc"
|
||||
tab = GenTable(
|
||||
@ -227,6 +227,7 @@ def list_users(
|
||||
"cas_allow_login": "CAS autorisé",
|
||||
"cas_allow_scodoc_login": "Cnx sans CAS",
|
||||
"cas_last_login": "Dernier login CAS",
|
||||
"edt_id": "Identifiant emploi du temps",
|
||||
},
|
||||
caption=title,
|
||||
page_title="title",
|
||||
@ -431,15 +432,3 @@ def check_modif_user(
|
||||
)
|
||||
# Roles ?
|
||||
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()
|
||||
|
@ -64,6 +64,7 @@ from config import Config
|
||||
from app import log, ScoDocJSONEncoder
|
||||
|
||||
from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_xml
|
||||
import sco_version
|
||||
|
||||
@ -204,6 +205,13 @@ class EtatAssiduite(int, BiDirectionalEnum):
|
||||
RETARD = 1
|
||||
ABSENT = 2
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
return {
|
||||
EtatAssiduite.PRESENT: "Présence",
|
||||
EtatAssiduite.ABSENT: "Absence",
|
||||
EtatAssiduite.RETARD: "Retard",
|
||||
}.get(self, "")
|
||||
|
||||
|
||||
class EtatJustificatif(int, BiDirectionalEnum):
|
||||
"""Code des états des justificatifs"""
|
||||
@ -215,6 +223,14 @@ class EtatJustificatif(int, BiDirectionalEnum):
|
||||
ATTENTE = 2
|
||||
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:
|
||||
"""
|
||||
@ -1139,6 +1155,61 @@ def abbrev_prenom(prenom):
|
||||
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():
|
||||
"representation du temps courant pour utilisateur"
|
||||
@ -1480,6 +1551,7 @@ def is_assiduites_module_forced(
|
||||
|
||||
def get_assiduites_time_config(config_type: str) -> str:
|
||||
from app.models import ScoDocSiteConfig
|
||||
|
||||
match config_type:
|
||||
case "matin":
|
||||
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")
|
||||
|
@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* DataTables style for ScoDoc gen_tables
|
||||
* generated using https://datatables.net/manual/styling/theme-creator
|
||||
* and customized by hand
|
||||
@ -138,111 +138,111 @@ table.dataTable.display tbody tr:hover.selected {
|
||||
background-color: #a9b7d1;
|
||||
}
|
||||
|
||||
table.dataTable.order-column tbody tr>.sorting_1,
|
||||
table.dataTable.order-column tbody tr>.sorting_2,
|
||||
table.dataTable.order-column tbody tr>.sorting_3,
|
||||
table.dataTable.display tbody tr>.sorting_1,
|
||||
table.dataTable.display tbody tr>.sorting_2,
|
||||
table.dataTable.display tbody tr>.sorting_3 {
|
||||
table.dataTable.order-column tbody tr > .sorting_1,
|
||||
table.dataTable.order-column tbody tr > .sorting_2,
|
||||
table.dataTable.order-column tbody tr > .sorting_3,
|
||||
table.dataTable.display tbody tr > .sorting_1,
|
||||
table.dataTable.display tbody tr > .sorting_2,
|
||||
table.dataTable.display tbody tr > .sorting_3 {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
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_3,
|
||||
table.dataTable.display tbody tr.selected>.sorting_1,
|
||||
table.dataTable.display tbody tr.selected>.sorting_2,
|
||||
table.dataTable.display tbody tr.selected>.sorting_3 {
|
||||
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_3,
|
||||
table.dataTable.display tbody tr.selected > .sorting_1,
|
||||
table.dataTable.display tbody tr.selected > .sorting_2,
|
||||
table.dataTable.display tbody tr.selected > .sorting_3 {
|
||||
background-color: #acbad4;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.odd>.sorting_1,
|
||||
table.dataTable.order-column.stripe tbody tr.odd>.sorting_1 {
|
||||
table.dataTable.display tbody tr.odd > .sorting_1,
|
||||
table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.odd>.sorting_2,
|
||||
table.dataTable.order-column.stripe tbody tr.odd>.sorting_2 {
|
||||
table.dataTable.display tbody tr.odd > .sorting_2,
|
||||
table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.odd>.sorting_3,
|
||||
table.dataTable.order-column.stripe tbody tr.odd>.sorting_3 {
|
||||
table.dataTable.display tbody tr.odd > .sorting_3,
|
||||
table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.odd.selected>.sorting_1,
|
||||
table.dataTable.order-column.stripe 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 {
|
||||
background-color: #a6b3cd;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.odd.selected>.sorting_2,
|
||||
table.dataTable.order-column.stripe 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 {
|
||||
background-color: #a7b5ce;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.odd.selected>.sorting_3,
|
||||
table.dataTable.order-column.stripe 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 {
|
||||
background-color: #a9b6d0;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.even>.sorting_1,
|
||||
table.dataTable.order-column.stripe tbody tr.even>.sorting_1 {
|
||||
table.dataTable.display tbody tr.even > .sorting_1,
|
||||
table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.even>.sorting_2,
|
||||
table.dataTable.order-column.stripe tbody tr.even>.sorting_2 {
|
||||
table.dataTable.display tbody tr.even > .sorting_2,
|
||||
table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.even>.sorting_3,
|
||||
table.dataTable.order-column.stripe tbody tr.even>.sorting_3 {
|
||||
table.dataTable.display tbody tr.even > .sorting_3,
|
||||
table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.even.selected>.sorting_1,
|
||||
table.dataTable.order-column.stripe 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 {
|
||||
background-color: #acbad4;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.even.selected>.sorting_2,
|
||||
table.dataTable.order-column.stripe 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 {
|
||||
background-color: #adbbd6;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr.even.selected>.sorting_3,
|
||||
table.dataTable.order-column.stripe 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 {
|
||||
background-color: #afbdd8;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr:hover>.sorting_1,
|
||||
table.dataTable.order-column.hover tbody tr:hover>.sorting_1 {
|
||||
table.dataTable.display tbody tr:hover > .sorting_1,
|
||||
table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr:hover>.sorting_2,
|
||||
table.dataTable.order-column.hover tbody tr:hover>.sorting_2 {
|
||||
table.dataTable.display tbody tr:hover > .sorting_2,
|
||||
table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr:hover>.sorting_3,
|
||||
table.dataTable.order-column.hover tbody tr:hover>.sorting_3 {
|
||||
table.dataTable.display tbody tr:hover > .sorting_3,
|
||||
table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr:hover.selected>.sorting_1,
|
||||
table.dataTable.order-column.hover 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 {
|
||||
background-color: #a1aec7;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr:hover.selected>.sorting_2,
|
||||
table.dataTable.order-column.hover 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 {
|
||||
background-color: #a2afc8;
|
||||
}
|
||||
|
||||
table.dataTable.display tbody tr:hover.selected>.sorting_3,
|
||||
table.dataTable.order-column.hover 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 {
|
||||
background-color: #a4b2cb;
|
||||
}
|
||||
|
||||
@ -419,7 +419,13 @@ table.dataTable td {
|
||||
color: #333333 !important;
|
||||
border: 1px solid #979797;
|
||||
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+ */
|
||||
background: -webkit-linear-gradient(top, white 0%, gainsboro 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
@ -447,7 +453,13 @@ table.dataTable td {
|
||||
color: white !important;
|
||||
border: 1px solid #111111;
|
||||
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+ */
|
||||
background: -webkit-linear-gradient(top, #585858 0%, #111111 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
@ -464,7 +476,13 @@ table.dataTable td {
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
|
||||
outline: none;
|
||||
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+ */
|
||||
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
@ -495,12 +513,50 @@ table.dataTable td {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
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-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%);
|
||||
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-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,
|
||||
@ -520,17 +576,69 @@ table.dataTable td {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>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 {
|
||||
.dataTables_wrapper
|
||||
.dataTables_scroll
|
||||
div.dataTables_scrollBody
|
||||
> 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;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>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 {
|
||||
.dataTables_wrapper
|
||||
.dataTables_scroll
|
||||
div.dataTables_scrollBody
|
||||
> 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;
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
@ -541,8 +649,8 @@ table.dataTable td {
|
||||
border-bottom: 1px solid #111111;
|
||||
}
|
||||
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollHead>table,
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollBody>table {
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollHead > table,
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@ -555,7 +663,6 @@ table.dataTable td {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
float: none;
|
||||
@ -568,7 +675,6 @@ table.dataTable td {
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
float: none;
|
||||
@ -580,8 +686,7 @@ table.dataTable td {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ------ Ajouts spécifiques pour ScoDoc:
|
||||
/* ------ Ajouts spécifiques pour ScoDoc:
|
||||
*/
|
||||
|
||||
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.order-column.stripe.hover tbody tr.odd:hover td.sorting_1 {
|
||||
background-color: rgb(80%, 85%, 80%);
|
||||
;
|
||||
}
|
||||
|
||||
/* 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.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
|
||||
background-color: rgb(85%, 85%, 85%);
|
||||
;
|
||||
}
|
||||
|
||||
/* Reglage largeur de la table */
|
||||
@ -652,4 +755,9 @@ table.dataTable.gt_table {
|
||||
/* Tables non centrées (inutile) */
|
||||
table.dataTable.gt_table.gt_left {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
table.dataTable.gt_table.gt_left td,
|
||||
table.dataTable.gt_table.gt_left th {
|
||||
text-align: left;
|
||||
}
|
||||
scodoc;css
|
||||
|
@ -1133,7 +1133,8 @@ a.redlink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.discretelink {
|
||||
a.discretelink,
|
||||
a:discretelink:visited {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
@ -1567,6 +1568,7 @@ h2.formsemestre,
|
||||
#gtrcontent h2 {
|
||||
margin-top: 2px;
|
||||
font-size: 130%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.formsemestre_page_title table.semtitle,
|
||||
@ -4347,6 +4349,10 @@ table.dataTable td.group {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#zonePartitions .edt_id {
|
||||
color: rgb(85, 255, 24);
|
||||
}
|
||||
|
||||
/* ------------- Nouveau tableau recap ------------ */
|
||||
div.table_recap {
|
||||
margin-top: 6px;
|
||||
@ -4856,7 +4862,3 @@ div.cas_etat_certif_ssl {
|
||||
font-style: italic;
|
||||
color: rgb(231, 0, 0);
|
||||
}
|
||||
|
||||
.edt_id {
|
||||
color: rgb(85, 255, 24);
|
||||
}
|
||||
|
@ -448,6 +448,13 @@ class ScoDocDateTimePicker extends HTMLElement {
|
||||
|
||||
// Ajouter le style au shadow DOM
|
||||
shadow.appendChild(style);
|
||||
|
||||
//Si une value est donnée
|
||||
|
||||
let value = this.getAttribute("value");
|
||||
if (value != null) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
@ -474,7 +481,7 @@ class ScoDocDateTimePicker extends HTMLElement {
|
||||
} else {
|
||||
// Mettre à jour la valeur de l'input caché avant la soumission
|
||||
this.hiddenInput.value = this.isValid()
|
||||
? this.valueAsDate.toIsoUtcString()
|
||||
? this.valueAsDate.toFakeIso()
|
||||
: "";
|
||||
}
|
||||
});
|
||||
|
@ -6,9 +6,10 @@
|
||||
|
||||
"""Liste simple d'étudiants
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from flask import g, url_for
|
||||
from app.models import Identite
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.tables import table_builder as tb
|
||||
|
||||
|
||||
@ -26,6 +27,7 @@ class TableEtud(tb.Table):
|
||||
with_foot_titles=False,
|
||||
**kwargs,
|
||||
):
|
||||
etuds = etuds or []
|
||||
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
classes = classes or ["gt_table", "gt_left"]
|
||||
super().__init__(
|
||||
@ -46,10 +48,12 @@ class TableEtud(tb.Table):
|
||||
|
||||
class RowEtud(tb.Row):
|
||||
"Ligne de la table d'étudiants"
|
||||
|
||||
# pour le moment très simple, extensible (codes, liens bulletins, ...)
|
||||
def __init__(self, table: TableEtud, etud: Identite, *args, **kwargs):
|
||||
super().__init__(table, etud.id, *args, **kwargs)
|
||||
self.etud = etud
|
||||
self.target_url = etud.url_fiche()
|
||||
|
||||
def add_etud_cols(self):
|
||||
"""Ajoute colonnes étudiant: codes, noms"""
|
||||
@ -77,7 +81,6 @@ class RowEtud(tb.Row):
|
||||
# formsemestre_id=res.formsemestre.id,
|
||||
# etudid=etud.id,
|
||||
# )
|
||||
url_bulletin = None # pour extension future
|
||||
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||
self.add_cell(
|
||||
"nom_disp",
|
||||
@ -85,23 +88,10 @@ class RowEtud(tb.Row):
|
||||
etud.nom_disp(),
|
||||
"identite_detail",
|
||||
data={"order": etud.sort_key},
|
||||
target=url_bulletin,
|
||||
target=self.target_url,
|
||||
target_attrs={"class": "etudinfo discretelink", "id": str(etud.id)},
|
||||
)
|
||||
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]:
|
||||
@ -115,3 +105,73 @@ def html_table_etuds(etudids) -> str:
|
||||
etuds = etuds_sorted_from_ids(etudids)
|
||||
table = TableEtud(etuds)
|
||||
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
|
||||
|
@ -5,7 +5,7 @@ from datetime import datetime
|
||||
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif
|
||||
from flask_sqlalchemy.query import Query, Pagination
|
||||
from sqlalchemy import union, literal, select, desc
|
||||
from app import db
|
||||
from app import db, g
|
||||
from flask import url_for
|
||||
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.
|
||||
"""
|
||||
|
||||
NB_PAR_PAGE: int = 2
|
||||
NB_PAR_PAGE: int = 25
|
||||
MAX_PAR_PAGE: int = 200
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*etudiants: tuple[Identite],
|
||||
table_data: "Data",
|
||||
filtre: "Filtre" = None,
|
||||
page: int = 1,
|
||||
nb_par_page: int = None,
|
||||
options: "Options" = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
@ -33,14 +33,12 @@ class ListeAssiJusti(tb.Table):
|
||||
filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None.
|
||||
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
|
||||
self.filtre = filtre if filtre is not None else Filtre()
|
||||
# Gestion de la pagination (par défaut page 1)
|
||||
self.page: int = page
|
||||
self.nb_par_page: int = (
|
||||
nb_par_page if nb_par_page is not None else ListeAssiJusti.NB_PAR_PAGE
|
||||
)
|
||||
|
||||
# Gestion des options, par défaut un objet Options vide
|
||||
self.options = options if options is not None else Options()
|
||||
|
||||
self.total_page: int = None
|
||||
|
||||
@ -57,9 +55,6 @@ class ListeAssiJusti(tb.Table):
|
||||
|
||||
self.ajouter_lignes()
|
||||
|
||||
def etudiant_seul(self) -> bool:
|
||||
return len(self.etudiants) == 1
|
||||
|
||||
def ajouter_lignes(self):
|
||||
# Générer les query assiduités et justificatifs
|
||||
assiduites_query_etudiants: Query = None
|
||||
@ -69,13 +64,21 @@ class ListeAssiJusti(tb.Table):
|
||||
type_obj = self.filtre.type_obj()
|
||||
|
||||
if type_obj in [0, 1]:
|
||||
assiduites_query_etudiants = Assiduite.query.filter(
|
||||
Assiduite.etudid.in_([e.etudid for e in self.etudiants])
|
||||
)
|
||||
assiduites_query_etudiants = self.table_data.assiduites_query
|
||||
|
||||
# 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]:
|
||||
justificatifs_query_etudiants = Justificatif.query.filter(
|
||||
Justificatif.etudid.in_([e.etudid for e in self.etudiants])
|
||||
)
|
||||
justificatifs_query_etudiants = self.table_data.justificatifs_query
|
||||
|
||||
# Combinaison des requêtes
|
||||
|
||||
@ -112,7 +115,7 @@ class ListeAssiJusti(tb.Table):
|
||||
résultats paginés.
|
||||
"""
|
||||
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):
|
||||
@ -149,7 +152,7 @@ class ListeAssiJusti(tb.Table):
|
||||
|
||||
# Définir les colonnes pour la requête d'assiduité
|
||||
if query_assiduite:
|
||||
query_assiduite = query_assiduite.with_entities(
|
||||
assiduites_entities: list = [
|
||||
Assiduite.assiduite_id.label("obj_id"),
|
||||
Assiduite.etudid.label("etudid"),
|
||||
Assiduite.entry_date.label("entry_date"),
|
||||
@ -159,12 +162,17 @@ class ListeAssiJusti(tb.Table):
|
||||
literal("assiduite").label("type"),
|
||||
Assiduite.est_just.label("est_just"),
|
||||
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)
|
||||
|
||||
# Définir les colonnes pour la requête de justificatif
|
||||
if query_justificatif:
|
||||
query_justificatif = query_justificatif.with_entities(
|
||||
justificatifs_entities: list = [
|
||||
Justificatif.justif_id.label("obj_id"),
|
||||
Justificatif.etudid.label("etudid"),
|
||||
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é
|
||||
literal(None).label("est_just"),
|
||||
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)
|
||||
|
||||
@ -206,8 +221,8 @@ class RowAssiJusti(tb.Row):
|
||||
def ajouter_colonnes(self, lien_redirection: str = None):
|
||||
# Ajout de l'étudiant
|
||||
self.table: ListeAssiJusti
|
||||
if not self.table.etudiant_seul():
|
||||
self._etud()
|
||||
if self.table.options.show_etu:
|
||||
self._etud(lien_redirection)
|
||||
|
||||
# Type d'objet
|
||||
self._type()
|
||||
@ -218,28 +233,37 @@ class RowAssiJusti(tb.Row):
|
||||
"Date de début",
|
||||
self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"),
|
||||
data={"order": self.ligne["date_debut"]},
|
||||
raw_content=self.ligne["date_debut"],
|
||||
)
|
||||
# Date de fin
|
||||
self.add_cell(
|
||||
"date_fin",
|
||||
"Date de fin",
|
||||
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
|
||||
raw_content=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
|
||||
self.add_cell(
|
||||
"entry_date",
|
||||
"Saisie le",
|
||||
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"),
|
||||
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:
|
||||
obj_type: str = ""
|
||||
is_assiduite: bool = self.ligne["type"] == "assiduite"
|
||||
@ -264,7 +288,7 @@ class RowAssiJusti(tb.Row):
|
||||
|
||||
self.add_cell("obj_type", "Type", obj_type)
|
||||
|
||||
def _etud(self) -> None:
|
||||
def _etud(self, lien_redirection) -> None:
|
||||
etud = self.etud
|
||||
self.table.group_titles.update(
|
||||
{
|
||||
@ -297,6 +321,21 @@ class RowAssiJusti(tb.Row):
|
||||
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:
|
||||
utilisateur: User = User.query.get(self.ligne["user_id"])
|
||||
|
||||
@ -304,11 +343,44 @@ class RowAssiJusti(tb.Row):
|
||||
"user",
|
||||
"Saisie par",
|
||||
"Inconnu" if utilisateur is None else utilisateur.get_nomprenom(),
|
||||
classes=["small-font"],
|
||||
)
|
||||
|
||||
def _actions(self) -> None:
|
||||
# XXX Ajouter une colonne avec les liens d'action (supprimer, modifier)
|
||||
pass
|
||||
url: str
|
||||
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", " ".join(html), no_excel=True)
|
||||
|
||||
|
||||
class Filtre:
|
||||
@ -323,7 +395,6 @@ class Filtre:
|
||||
entry_date: tuple[int, datetime] = None,
|
||||
date_debut: tuple[int, datetime] = None,
|
||||
date_fin: tuple[int, datetime] = None,
|
||||
etats: list[EtatAssiduite | EtatJustificatif] = None,
|
||||
) -> None:
|
||||
"""
|
||||
__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.
|
||||
"""
|
||||
|
||||
self.filtres = {}
|
||||
self.filtres = {"type_obj": type_obj}
|
||||
|
||||
if entry_date is not None:
|
||||
self.filtres["entry_date"]: tuple[int, datetime] = entry_date
|
||||
@ -347,9 +418,6 @@ class Filtre:
|
||||
if date_fin is not None:
|
||||
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:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
@ -84,6 +84,8 @@ class Table(Element):
|
||||
self.row_by_id: dict[str, "Row"] = {}
|
||||
self.column_ids = []
|
||||
"ordered list of columns ids"
|
||||
self.raw_column_ids = []
|
||||
"ordered list of columns ids for excel"
|
||||
self.groups = []
|
||||
"ordered list of column groups names"
|
||||
self.group_titles = {}
|
||||
@ -360,6 +362,7 @@ class Row(Element):
|
||||
target_attrs: dict = None,
|
||||
target: str = None,
|
||||
column_classes: set[str] = None,
|
||||
no_excel: bool = False,
|
||||
) -> "Cell":
|
||||
"""Create cell and add it to the row.
|
||||
group: groupe de colonnes
|
||||
@ -380,10 +383,17 @@ class Row(Element):
|
||||
target=target,
|
||||
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(
|
||||
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":
|
||||
"""Add a cell to the row.
|
||||
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
|
||||
if col_id not in self.table.column_ids:
|
||||
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)
|
||||
if column_group is not None:
|
||||
self.table.column_group[col_id] = column_group
|
||||
@ -422,7 +435,7 @@ class Row(Element):
|
||||
"""row as a dict, with only cell contents"""
|
||||
return {
|
||||
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:
|
||||
|
@ -1,14 +1,15 @@
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="pageContent">
|
||||
<h3>Ajouter une assiduité</h3>
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
<h3>Signaler un évènement pour {{etud.html_link_fiche()|safe}}</h3>
|
||||
{% if saisie_eval %}
|
||||
<div id="saisie_eval">
|
||||
<br>
|
||||
<h3>
|
||||
La saisie de l'assiduité 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
|
||||
La saisie a été préconfigurée en fonction de l'évaluation. <br>
|
||||
Une fois la saisie terminée, cliquez sur le lien ci-dessous
|
||||
</h3>
|
||||
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
|
||||
</div>
|
||||
@ -29,7 +30,7 @@
|
||||
|
||||
<div class="assi-row">
|
||||
<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">
|
||||
<option value="absent" selected>Absent</option>
|
||||
<option value="retard">Retard</option>
|
||||
@ -54,7 +55,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -63,8 +64,7 @@
|
||||
|
||||
</section>
|
||||
<section class="liste">
|
||||
<a class="icon filter" onclick="filterAssi()"></a>
|
||||
{% include "assiduites/widgets/tableau_assi.j2" %}
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@ -141,7 +141,7 @@
|
||||
let assiduite_id = null;
|
||||
|
||||
createAssiduiteComplete(assiduite, etudid);
|
||||
loadAll();
|
||||
updateTableau();
|
||||
btn.disabled = true;
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
@ -208,7 +208,6 @@
|
||||
{% endif %}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
loadAll();
|
||||
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
|
||||
dayOnly()
|
||||
|
||||
@ -231,4 +230,4 @@
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock pageContent %}
|
||||
{% endblock pageContent %}
|
||||
|
@ -2,8 +2,6 @@
|
||||
{% block pageContent %}
|
||||
<div class="pageContent">
|
||||
<h3>Justifier des absences ou retards</h3>
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
|
||||
|
||||
<section class="justi-form page">
|
||||
|
||||
@ -58,28 +56,9 @@
|
||||
|
||||
</section>
|
||||
<section class="liste">
|
||||
<a class="icon filter" onclick="filterJusti()"></a>
|
||||
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||
{{tableau | safe }}
|
||||
</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>
|
||||
|
||||
<style>
|
||||
@ -167,17 +146,15 @@
|
||||
processData: false,
|
||||
success: () => {
|
||||
pushToast(generateToast(document.createTextNode(`Importation du fichier : ${f.name} finie`)));
|
||||
loadAll();
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
});
|
||||
if (in_files.files.length == 0) {
|
||||
loadAll();
|
||||
}
|
||||
$.when(...requests).done(() => {
|
||||
location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
function validerFormulaire(btn) {
|
||||
@ -258,7 +235,6 @@
|
||||
const assi_evening = '{{assi_evening}}';
|
||||
|
||||
window.onload = () => {
|
||||
loadAll();
|
||||
document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() });
|
||||
dayOnly()
|
||||
|
||||
|
@ -2,95 +2,6 @@
|
||||
<div class="pageContent">
|
||||
|
||||
<h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
<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>
|
||||
{{tableau | safe }}
|
||||
</div>
|
||||
{% 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>
|
||||
{% endblock app_content %}
|
27
app/templates/assiduites/pages/tableau_actions.j2
Normal file
27
app/templates/assiduites/pages/tableau_actions.j2
Normal 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 %}
|
@ -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 %}
|
@ -3,6 +3,7 @@
|
||||
{% 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 %}
|
||||
@ -43,4 +44,4 @@
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -119,11 +119,17 @@
|
||||
}
|
||||
|
||||
|
||||
{% if moduleid %}
|
||||
const moduleimpl_dynamic_selector_id = "{{moduleid}}"
|
||||
{% else %}
|
||||
const moduleimpl_dynamic_selector_id = "moduleimpl_select"
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.getElementById('moduleimpl_select').addEventListener('change', (el) => {
|
||||
document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => {
|
||||
const assi = getCurrentAssiduite(etudid);
|
||||
if (assi) {
|
||||
editAssiduite(assi.assiduite_id, assi.etat, [assi]);
|
||||
|
@ -1,6 +1,8 @@
|
||||
<select name="moduleimpl_select" id="moduleimpl_select">
|
||||
|
||||
{% with moduleimpl_id=moduleimpl_id %}
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
{% endwith %}
|
||||
|
||||
{% for mod in modules %}
|
||||
{% if mod.moduleimpl_id == moduleimpl_id %}
|
||||
|
@ -1,6 +1,10 @@
|
||||
{% 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 %}
|
||||
<option value="" selected> Non spécifié </option>
|
||||
<option value=""> Non spécifié </option>
|
||||
{% 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 %}
|
73
app/templates/assiduites/widgets/tableau.j2
Normal file
73
app/templates/assiduites/widgets/tableau.j2
Normal 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>
|
106
app/templates/assiduites/widgets/tableau_actions/details.j2
Normal file
106
app/templates/assiduites/widgets/tableau_actions/details.j2
Normal 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>
|
107
app/templates/assiduites/widgets/tableau_actions/modifier.j2
Normal file
107
app/templates/assiduites/widgets/tableau_actions/modifier.j2
Normal 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>
|
@ -9,15 +9,18 @@
|
||||
<div class="user_basics">
|
||||
<b>Login :</b> {{user.user_name}}<br>
|
||||
<b>CAS id:</b> {{user.cas_id or "(aucun)"}}
|
||||
(CAS {{'autorisé' if user.cas_allow_login else 'interdit'}} pour cet utilisateur)
|
||||
{% if user.cas_allow_scodoc_login %}
|
||||
(connexion sans CAS autorisée)
|
||||
{% if ScoDocSiteConfig.is_cas_enabled() %}
|
||||
(CAS {{'autorisé' if user.cas_allow_login else 'interdit'}} pour cet utilisateur)
|
||||
{% if user.cas_allow_scodoc_login %}
|
||||
(connexion sans CAS autorisée)
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<b>Nom :</b> {{user.nom or ""}}<br>
|
||||
<b>Prénom :</b> {{user.prenom or ""}}<br>
|
||||
<b>Mail :</b> {{user.email}}<br>
|
||||
<b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br>
|
||||
<b>Identifiant EDT:</b> {{user.edt_id or ""}}<br>
|
||||
<b>Rôles :</b> {{user.get_roles_string()}}<br>
|
||||
<b>Dept :</b> {{user.dept or ""}}<br>
|
||||
{% if user.passwd_temp or user.password_scodoc7 %}
|
||||
|
20
app/templates/scolar/export_etudiants_courants.j2
Normal file
20
app/templates/scolar/export_etudiants_courants.j2
Normal 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 %}
|
@ -617,7 +617,7 @@
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
.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);
|
||||
|
||||
// Lancement de l'édition du nom
|
||||
divGroupe.querySelector(".modif").click();
|
||||
// divGroupe.querySelector(".modif").click();
|
||||
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
.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 => {
|
||||
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();
|
||||
})
|
||||
.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 => {
|
||||
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();
|
||||
})
|
||||
@ -916,12 +916,12 @@
|
||||
.then(r => { return r.json() })
|
||||
.then(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();
|
||||
})
|
||||
.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>";
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ from flask import abort, url_for, redirect
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.decorators import (
|
||||
@ -47,6 +48,10 @@ from app.models import (
|
||||
Departement,
|
||||
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 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.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
|
||||
|
||||
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||
@ -260,13 +266,6 @@ def signal_assiduites_etud():
|
||||
if etud.dept_id != g.scodoc_dept_id:
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
morning = ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00")
|
||||
lunch = ScoDocSiteConfig.assi_get_rounded_time("assi_lunch_time", "13:00:00")
|
||||
afternoon = ScoDocSiteConfig.assi_get_rounded_time(
|
||||
"assi_afternoon_time", "18:00:00"
|
||||
tableau = _preparer_tableau(
|
||||
liste_assi.Data.from_etudiants(
|
||||
etud,
|
||||
),
|
||||
filename=f"assiduite-{etudid}",
|
||||
afficher_etu=False,
|
||||
filtre=liste_assi.Filtre(type_obj=1),
|
||||
options=liste_assi.Options(show_module=True),
|
||||
)
|
||||
|
||||
# Gestion du selecteur de moduleimpl (pour le tableau différé)
|
||||
select = f"""
|
||||
<select class="dynaSelect">
|
||||
{render_template("assiduites/widgets/simplemoduleimpl_select.j2")}
|
||||
</select>
|
||||
"""
|
||||
|
||||
if not tableau[0]:
|
||||
return tableau[1]
|
||||
# Génération de la page
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
@ -330,8 +325,10 @@ def signal_assiduites_etud():
|
||||
saisie_eval=saisie_eval,
|
||||
date_deb=date_deb,
|
||||
date_fin=date_fin,
|
||||
etud=etud,
|
||||
redirect_url=redirect_url,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
tableau=tableau[1],
|
||||
),
|
||||
# render_template(
|
||||
# "assiduites/pages/signal_assiduites_etud.j2",
|
||||
@ -377,7 +374,7 @@ def liste_assiduites_etud():
|
||||
if etud.dept_id != g.scodoc_dept_id:
|
||||
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)
|
||||
|
||||
# Préparation de la page
|
||||
@ -393,18 +390,25 @@ def liste_assiduites_etud():
|
||||
"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
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
render_template(
|
||||
"assiduites/pages/liste_assiduites.j2",
|
||||
sco=ScoData(etud),
|
||||
date=datetime.date.today().isoformat(),
|
||||
assi_id=assiduite_id,
|
||||
assi_limit_annee=sco_preferences.get_preference(
|
||||
"assi_limit_annee",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
tableau=tableau[1],
|
||||
),
|
||||
).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
|
||||
return HTMLBuilder(
|
||||
header,
|
||||
@ -513,6 +530,7 @@ def ajout_justificatif_etud():
|
||||
),
|
||||
assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
|
||||
assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),
|
||||
tableau=tableau[1],
|
||||
),
|
||||
).build()
|
||||
|
||||
@ -1044,26 +1062,44 @@ def visu_assi_group():
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/testTableau")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def testTableau():
|
||||
"""Visualisation de l'assiduité d'un groupe entre deux dates"""
|
||||
def _preparer_tableau(
|
||||
data: liste_assi.Data,
|
||||
filename: str = "tableau-assiduites",
|
||||
afficher_etu: bool = True,
|
||||
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(
|
||||
"etudid", 18114
|
||||
) # TODO retirer la valeur par défaut de test
|
||||
Cette fontion récupère dans la requête les arguments :
|
||||
|
||||
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_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")
|
||||
# Vérification de nb_ligne_page
|
||||
try:
|
||||
nb_ligne_page: int = int(nb_ligne_page)
|
||||
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)
|
||||
# Vérification de page_number
|
||||
@ -1072,33 +1108,272 @@ def testTableau():
|
||||
except (ValueError, TypeError):
|
||||
page_number = 1
|
||||
|
||||
from app.tables.liste_assiduites import ListeAssiJusti
|
||||
fmt = request.args.get("fmt", "html")
|
||||
|
||||
table: ListeAssiJusti = ListeAssiJusti(
|
||||
Identite.get_etud(etudid), page=page_number, nb_par_page=nb_ligne_page
|
||||
if options is None:
|
||||
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"):
|
||||
return scu.send_file(
|
||||
return False, scu.send_file(
|
||||
table.excel(),
|
||||
filename=f"assiduite-{groups_infos.groups_filename}",
|
||||
filename=filename,
|
||||
mime=scu.XLSX_MIMETYPE,
|
||||
suffix=scu.XLSX_SUFFIX,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/test_assi.j2",
|
||||
sco=ScoData(),
|
||||
return True, render_template(
|
||||
"assiduites/widgets/tableau.j2",
|
||||
tableau=table.html(),
|
||||
title=f"Test tableau",
|
||||
total_pages=table.total_pages,
|
||||
page_number=page_number,
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
nb_ligne_page=nb_ligne_page,
|
||||
options=options,
|
||||
afficher_options=afficher_options,
|
||||
)
|
||||
|
||||
|
||||
@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")
|
||||
@scodoc
|
||||
@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
|
||||
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]] = []
|
||||
# Récupération de l'id et d'un nom lisible pour chaque moduleimpl
|
||||
for modimpl in modimpls_list:
|
||||
|
@ -105,6 +105,7 @@ from app.scodoc import sco_synchro_etuds
|
||||
from app.scodoc import sco_trombino
|
||||
from app.scodoc import sco_trombino_tours
|
||||
from app.scodoc import sco_up_to_date
|
||||
from app.tables import list_etuds
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@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"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@ -2361,35 +2389,57 @@ def form_students_import_infos_admissions(formsemestre_id=None):
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudChangeAdr)
|
||||
@scodoc7func
|
||||
def formsemestre_import_etud_admission(formsemestre_id, import_email=True):
|
||||
"""Ré-importe donnees admissions par synchro Portail Apogée"""
|
||||
(
|
||||
no_nip,
|
||||
unknowns,
|
||||
changed_mails,
|
||||
) = sco_synchro_etuds.formsemestre_import_etud_admission(
|
||||
formsemestre_id, import_identite=True, import_email=import_email
|
||||
)
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Ré-import données admission"),
|
||||
"<h3>Opération effectuée</h3>",
|
||||
]
|
||||
if no_nip:
|
||||
H.append("<p>Attention: étudiants sans NIP: " + str(no_nip) + "</p>")
|
||||
if unknowns:
|
||||
H.append(
|
||||
"<p>Attention: étudiants inconnus du portail: codes NIP="
|
||||
+ str(unknowns)
|
||||
+ "</p>"
|
||||
def formsemestre_import_etud_admission(
|
||||
formsemestre_id=None, import_email=True, tous_courants=False
|
||||
):
|
||||
"""Ré-importe donnees admissions par synchro Portail Apogée.
|
||||
Si tous_courants, le fait pour tous les formsemestres courants du département
|
||||
"""
|
||||
if tous_courants:
|
||||
departement = Departement.query.get(g.scodoc_dept_id)
|
||||
formsemestres = FormSemestre.get_dept_formsemestres_courants(departement)
|
||||
else:
|
||||
formsemestres = [FormSemestre.get_formsemestre(formsemestre_id)]
|
||||
|
||||
diag_by_sem = {}
|
||||
for formsemestre in formsemestres:
|
||||
(
|
||||
etuds_no_nip,
|
||||
etuds_unknown,
|
||||
changed_mails,
|
||||
) = sco_synchro_etuds.formsemestre_import_etud_admission(
|
||||
formsemestre.id, import_identite=True, import_email=import_email
|
||||
)
|
||||
if changed_mails:
|
||||
H.append("<h3>Adresses mails modifiées:</h3><ul>")
|
||||
for etud, old_mail in changed_mails:
|
||||
H.append(
|
||||
f"""<li>{etud.nom}: <tt>{old_mail}</tt> devient <tt>{etud.email}</tt></li>"""
|
||||
)
|
||||
H.append("</ul>")
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
diag = ""
|
||||
if etuds_no_nip:
|
||||
diag += f"""<p>Attention: étudiants sans NIP:
|
||||
{', '.join([e.html_link_fiche() for e in etuds_no_nip])}
|
||||
</p>"""
|
||||
|
||||
if etuds_unknown:
|
||||
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(
|
||||
|
@ -450,6 +450,17 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
"readonly": edit_only_roles,
|
||||
},
|
||||
),
|
||||
(
|
||||
"edt_id",
|
||||
{
|
||||
"title": "Identifiant sur l'emploi du temps",
|
||||
"input_type": "text",
|
||||
"explanation": """id du compte utilisateur sur l'emploi du temps
|
||||
ou l'annuaire de l'établissement (par défaut, l'e-mail institutionnel )""",
|
||||
"size": 36,
|
||||
"allow_null": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
if not edit: # options création utilisateur
|
||||
descr += [
|
||||
@ -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: previous_values={initvalues}")
|
||||
log(f"sco_users: new_values={vals}")
|
||||
sco_users.user_edit(user_name, vals)
|
||||
flash(f"Utilisateur {user_name} modifié")
|
||||
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(
|
||||
url_for(
|
||||
"users.user_info_page",
|
||||
@ -749,7 +762,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
log(
|
||||
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)
|
||||
db.session.add(the_user)
|
||||
db.session.commit()
|
||||
@ -916,11 +929,12 @@ def user_info_page(user_name=None):
|
||||
|
||||
return render_template(
|
||||
"auth/user_info_page.j2",
|
||||
user=user,
|
||||
title=f"Utilisateur {user.user_name}",
|
||||
Permission=Permission,
|
||||
dept=dept,
|
||||
Permission=Permission,
|
||||
ScoDocSiteConfig=ScoDocSiteConfig,
|
||||
session_info=session_info,
|
||||
title=f"Utilisateur {user.user_name}",
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
[pytest]
|
||||
norecursedirs = .git app/static
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
apo
|
||||
@ -11,4 +12,4 @@ markers =
|
||||
|
||||
filterwarnings =
|
||||
ignore:.*json.*:DeprecationWarning
|
||||
# en attendant mise à jour de Flask-JSON
|
||||
# en attendant mise à jour de Flask-JSON
|
||||
|
@ -88,7 +88,7 @@ python-editor==1.0.4
|
||||
pytz==2023.3.post1
|
||||
PyYAML==6.0.1
|
||||
redis==5.0.1
|
||||
reportlab==4.0.5
|
||||
reportlab==4.0.7
|
||||
requests==2.31.0
|
||||
rq==1.15.1
|
||||
six==1.16.0
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.60"
|
||||
SCOVERSION = "9.6.64"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -28,10 +28,12 @@ from tests.api.setup_test_api import (
|
||||
API_URL,
|
||||
API_USER_ADMIN,
|
||||
CHECK_CERTIFICATE,
|
||||
DEPT_ACRONYM,
|
||||
GET,
|
||||
POST_JSON,
|
||||
api_headers,
|
||||
get_auth_headers,
|
||||
)
|
||||
from tests.api.setup_test_api import api_headers # pylint: disable=unused-import
|
||||
from tests.api.tools_test_api import (
|
||||
BULLETIN_ETUDIANT_FIELDS,
|
||||
BULLETIN_FIELDS,
|
||||
@ -923,3 +925,85 @@ def test_etudiant_groups(api_headers):
|
||||
group = groups[0]
|
||||
fields_ok = verify_fields(group, fields)
|
||||
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"]
|
||||
|
@ -90,6 +90,7 @@ def test_formsemestre_partition(api_headers):
|
||||
)
|
||||
assert isinstance(group_r, dict)
|
||||
assert group_r["group_name"] == group_d["group_name"]
|
||||
assert group_r["edt_id"] is None
|
||||
# --- Liste groupes de la partition
|
||||
partition = GET(f"/partition/{partition_r['id']}", headers=headers)
|
||||
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
|
||||
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
|
||||
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=headers)[0]
|
||||
repl = POST_JSON(f"/group/{group['id']}/set_etudiant/{etud['id']}", headers=headers)
|
||||
|
@ -88,15 +88,17 @@ def test_edit_users(api_admin_headers):
|
||||
# Change le dept et rend inactif
|
||||
user = POST_JSON(
|
||||
f"/user/{user['id']}/edit",
|
||||
{"active": False, "dept": "TAPI"},
|
||||
{"active": False, "dept": "TAPI", "edt_id": "GGG"},
|
||||
headers=admin_h,
|
||||
)
|
||||
assert user["dept"] == "TAPI"
|
||||
assert user["active"] is False
|
||||
assert user["edt_id"] == "GGG"
|
||||
user = GET(f"/user/{user['id']}", headers=admin_h)
|
||||
assert user["nom"] == "Toto"
|
||||
assert user["dept"] == "TAPI"
|
||||
assert user["active"] is False
|
||||
assert user["edt_id"] == "GGG"
|
||||
|
||||
|
||||
def test_roles(api_admin_headers):
|
||||
|
@ -47,6 +47,7 @@ def test_identite(test_client):
|
||||
assert e.prenom == "PRENOM"
|
||||
assert e.prenom_etat_civil == "PRENOM_ETAT_CIVIL"
|
||||
assert e.dept_naissance == "dept_naissance"
|
||||
assert e.etat_civil == "PRENOM_ETAT_CIVIL NOM"
|
||||
#
|
||||
admission_id = e.admission_id
|
||||
admission = db.session.get(Admission, admission_id)
|
||||
@ -81,32 +82,32 @@ def test_etat_civil(test_client):
|
||||
dept = Departement.query.first()
|
||||
args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id}
|
||||
# Homme
|
||||
e = Identite(**args)
|
||||
db.session.add(e)
|
||||
e = Identite.create_etud(**args)
|
||||
db.session.flush()
|
||||
assert e.civilite_etat_civil_str == "M."
|
||||
assert e.e == ""
|
||||
assert e.etat_civil == "M. PRÉNOM NOM"
|
||||
# Femme
|
||||
e = Identite(**args | {"civilite": "F"})
|
||||
db.session.add(e)
|
||||
e = Identite.create_etud(**args | {"civilite": "F"})
|
||||
db.session.flush()
|
||||
assert e.civilite_etat_civil_str == "Mme"
|
||||
assert e.e == "e"
|
||||
assert e.etat_civil == "Mme PRÉNOM NOM"
|
||||
# Homme devenu femme
|
||||
e = Identite(**(args | {"civilite_etat_civil": "F"}))
|
||||
db.session.add(e)
|
||||
e = Identite.create_etud(**(args | {"civilite_etat_civil": "F"}))
|
||||
db.session.flush()
|
||||
assert e.civilite_etat_civil_str == "Mme"
|
||||
assert e.civilite_str == "M."
|
||||
assert e.e == ""
|
||||
assert e.etat_civil == "Mme PRÉNOM NOM"
|
||||
# Femme devenue neutre
|
||||
e = Identite(**(args | {"civilite": "X", "civilite_etat_civil": "F"}))
|
||||
db.session.add(e)
|
||||
e = Identite.create_etud(**(args | {"civilite": "X", "civilite_etat_civil": "F"}))
|
||||
db.session.flush()
|
||||
assert e.civilite_etat_civil_str == "Mme"
|
||||
assert e.civilite_str == ""
|
||||
assert e.e == "(e)"
|
||||
assert e.prenom_etat_civil is None
|
||||
assert e.etat_civil == "Mme PRÉNOM NOM"
|
||||
# La version dict
|
||||
e_d = e.to_dict_scodoc7()
|
||||
assert e_d["civilite"] == "X"
|
||||
@ -119,7 +120,7 @@ def test_etud_legacy(test_client):
|
||||
dept = Departement.query.first()
|
||||
args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id}
|
||||
# Prénom état civil
|
||||
e = Identite(**(args))
|
||||
e = Identite.create_etud(**(args))
|
||||
db.session.add(e)
|
||||
db.session.flush()
|
||||
e_dict = e.to_dict_bul()
|
||||
|
@ -123,3 +123,30 @@ def test_create_delete(test_client):
|
||||
db.session.commit()
|
||||
ul = User.query.filter_by(prenom="Pierre").all()
|
||||
assert len(ul) == 1
|
||||
|
||||
|
||||
def test_edit(test_client):
|
||||
"test edition object utlisateur"
|
||||
args = {
|
||||
"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()
|
||||
|
Loading…
Reference in New Issue
Block a user