Merge remote-tracking branch 'scodoc/master' into pe-BUT-v2

# Conflicts:
#	app/pe/pe_jurype.py
This commit is contained in:
Cléo Baras 2024-01-25 21:22:33 +01:00
commit 83c6ec44c8
131 changed files with 4762 additions and 3729 deletions

View File

@ -3,9 +3,11 @@
from flask_json import as_json from flask_json import as_json
from flask import Blueprint from flask import Blueprint
from flask import request, g from flask import request, g
from flask_login import current_user
from app import db from app import db
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoException from app.scodoc.sco_exceptions import AccessDenied, ScoException
from app.scodoc.sco_permissions import Permission
api_bp = Blueprint("api", __name__) api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__) api_web_bp = Blueprint("apiweb", __name__)
@ -48,13 +50,21 @@ def requested_format(default_format="json", allowed_formats=None):
@as_json @as_json
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None): def get_model_api_object(
model_cls: db.Model,
model_id: int,
join_cls: db.Model = None,
restrict: bool | None = None,
):
""" """
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]" Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
L'agument restrict est passé to_dict, est signale que l'on veut une version restreinte
(sans données personnelles, ou sans informations sur le justificatif d'absence)
""" """
query = model_cls.query.filter_by(id=model_id) query = model_cls.query.filter_by(id=model_id)
if g.scodoc_dept and join_cls is not None: if g.scodoc_dept and join_cls is not None:
@ -66,8 +76,9 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
404, 404,
message=f"{model_cls.__name__} inexistant(e)", message=f"{model_cls.__name__} inexistant(e)",
) )
if restrict is None:
return unique.to_dict(format_api=True) return unique.to_dict(format_api=True)
return unique.to_dict(format_api=True, restrict=restrict)
from app.api import tokens from app.api import tokens

View File

@ -104,7 +104,8 @@ def etudiants_courants(long=False):
or_(Departement.acronym == acronym for acronym in allowed_depts) or_(Departement.acronym == acronym for acronym in allowed_depts)
) )
if long: if long:
data = [etud.to_dict_api() for etud in etuds] restrict = not current_user.has_permission(Permission.ViewEtudData)
data = [etud.to_dict_api(restrict=restrict) for etud in etuds]
else: else:
data = [etud.to_dict_short() for etud in etuds] data = [etud.to_dict_short() for etud in etuds]
return data return data
@ -138,8 +139,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
404, 404,
message="étudiant inconnu", message="étudiant inconnu",
) )
restrict = not current_user.has_permission(Permission.ViewEtudData)
return etud.to_dict_api() return etud.to_dict_api(restrict=restrict)
@bp.route("/etudiant/etudid/<int:etudid>/photo") @bp.route("/etudiant/etudid/<int:etudid>/photo")
@ -251,7 +252,8 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
query = query.join(Departement).filter( query = query.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts) or_(Departement.acronym == acronym for acronym in allowed_depts)
) )
return [etud.to_dict_api() for etud in query] restrict = not current_user.has_permission(Permission.ViewEtudData)
return [etud.to_dict_api(restrict=restrict) for etud in query]
@bp.route("/etudiants/name/<string:start>") @bp.route("/etudiants/name/<string:start>")
@ -278,7 +280,11 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
) )
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit) etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici: # Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))] restrict = not current_user.has_permission(Permission.ViewEtudData)
return [
etud.to_dict_api(restrict=restrict)
for etud in sorted(etuds, key=attrgetter("sort_key"))
]
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres") @bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@ -543,7 +549,8 @@ def etudiant_create(force=False):
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici # Note: je ne comprends pas pourquoi un refresh est nécessaire ici
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom'). # sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
db.session.refresh(etud) db.session.refresh(etud)
r = etud.to_dict_api()
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
return r return r
@ -590,5 +597,6 @@ def etudiant_edit(
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici # Note: je ne comprends pas pourquoi un refresh est nécessaire ici
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom'). # sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
db.session.refresh(etud) db.session.refresh(etud)
r = etud.to_dict_api() restrict = not current_user.has_permission(Permission.ViewEtudData)
r = etud.to_dict_api(restrict=restrict)
return r return r

View File

@ -67,7 +67,7 @@ def get_evaluation(evaluation_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def evaluations(moduleimpl_id: int): def moduleimpl_evaluations(moduleimpl_id: int):
""" """
Retourne la liste des évaluations d'un moduleimpl Retourne la liste des évaluations d'un moduleimpl
@ -75,14 +75,8 @@ def evaluations(moduleimpl_id: int):
Exemple de résultat : voir /evaluation Exemple de résultat : voir /evaluation
""" """
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if g.scodoc_dept: return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
return [e.to_dict_api() for e in query]
@bp.route("/evaluation/<int:evaluation_id>/notes") @bp.route("/evaluation/<int:evaluation_id>/notes")

View File

@ -11,7 +11,7 @@ from operator import attrgetter, itemgetter
from flask import g, make_response, request from flask import g, make_response, request
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import current_user, login_required
import app import app
from app import db from app import db
@ -360,7 +360,8 @@ def formsemestre_etudiants(
inscriptions = formsemestre.inscriptions inscriptions = formsemestre.inscriptions
if long: if long:
etuds = [ins.etud.to_dict_api() for ins in inscriptions] restrict = not current_user.has_permission(Permission.ViewEtudData)
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
else: else:
etuds = [ins.etud.to_dict_short() for ins in inscriptions] etuds = [ins.etud.to_dict_short() for ins in inscriptions]
# Ajout des groupes de chaque étudiants # Ajout des groupes de chaque étudiants

View File

@ -66,7 +66,7 @@ def _news_delete_jury_etud(etud: Identite):
"génère news sur effacement décision" "génère news sur effacement décision"
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API # n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
url = url_for( url = url_for(
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id "scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
) )
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_JURY, typ=ScolarNews.NEWS_JURY,

View File

@ -15,7 +15,7 @@ from werkzeug.exceptions import NotFound
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db from app import db, set_sco_dept
from app.api import api_bp as bp from app.api import api_bp as bp
from app.api import api_web_bp from app.api import api_web_bp
from app.api import get_model_api_object, tools from app.api import get_model_api_object, tools
@ -53,14 +53,19 @@ def justificatif(justif_id: int = None):
"date_fin": "2022-10-31T10:00+01:00", "date_fin": "2022-10-31T10:00+01:00",
"etat": "valide", "etat": "valide",
"fichier": "archive_id", "fichier": "archive_id",
"raison": "une raison", "raison": "une raison", // VIDE si pas le droit
"entry_date": "2022-10-31T08:00+01:00", "entry_date": "2022-10-31T08:00+01:00",
"user_id": 1 or null, "user_id": 1 or null,
} }
""" """
return get_model_api_object(Justificatif, justif_id, Identite) return get_model_api_object(
Justificatif,
justif_id,
Identite,
restrict=not current_user.has_permission(Permission.AbsJustifView),
)
# etudid # etudid
@ -133,8 +138,9 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
# Mise en forme des données puis retour en JSON # Mise en forme des données puis retour en JSON
data_set: list[dict] = [] data_set: list[dict] = []
restrict = not current_user.has_permission(Permission.AbsJustifView)
for just in justificatifs_query.all(): for just in justificatifs_query.all():
data = just.to_dict(format_api=True) data = just.to_dict(format_api=True, restrict=restrict)
data_set.append(data) data_set.append(data)
return data_set return data_set
@ -151,7 +157,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
@as_json @as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatifs_dept(dept_id: int = None, with_query: bool = False): def justificatifs_dept(dept_id: int = None, with_query: bool = False):
"""XXX TODO missing doc""" """
Renvoie tous les justificatifs d'un département
(en ajoutant un champ "formsemestre" si possible)
"""
# Récupération du département et des étudiants du département # Récupération du département et des étudiants du département
dept: Departement = Departement.query.get(dept_id) dept: Departement = Departement.query.get(dept_id)
@ -169,14 +178,15 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
justificatifs_query: Query = _filter_manager(request, justificatifs_query) justificatifs_query: Query = _filter_manager(request, justificatifs_query)
# Mise en forme des données et retour JSON # Mise en forme des données et retour JSON
restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = [] data_set: list[dict] = []
for just in justificatifs_query: for just in justificatifs_query:
data_set.append(_set_sems(just)) data_set.append(_set_sems(just, restrict=restrict))
return data_set return data_set
def _set_sems(justi: Justificatif) -> dict: def _set_sems(justi: Justificatif, restrict: bool) -> dict:
""" """
_set_sems Ajoute le formsemestre associé au justificatif s'il existe _set_sems Ajoute le formsemestre associé au justificatif s'il existe
@ -189,7 +199,7 @@ def _set_sems(justi: Justificatif) -> dict:
dict: La représentation de l'assiduité en dictionnaire dict: La représentation de l'assiduité en dictionnaire
""" """
# Conversion du justificatif en dictionnaire # Conversion du justificatif en dictionnaire
data = justi.to_dict(format_api=True) data = justi.to_dict(format_api=True, restrict=restrict)
# Récupération du formsemestre de l'assiduité # Récupération du formsemestre de l'assiduité
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict()) formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
@ -243,9 +253,10 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
justificatifs_query: Query = _filter_manager(request, justificatifs_query) justificatifs_query: Query = _filter_manager(request, justificatifs_query)
# Retour des justificatifs en JSON # Retour des justificatifs en JSON
restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = [] data_set: list[dict] = []
for justi in justificatifs_query.all(): for justi in justificatifs_query.all():
data = justi.to_dict(format_api=True) data = justi.to_dict(format_api=True, restrict=restrict)
data_set.append(data) data_set.append(data)
return data_set return data_set
@ -294,6 +305,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
404, 404,
message="étudiant inconnu", message="étudiant inconnu",
) )
set_sco_dept(etud.departement.acronym)
# Récupération des justificatifs à créer # Récupération des justificatifs à créer
create_list: list[object] = request.get_json(force=True) create_list: list[object] = request.get_json(force=True)

View File

@ -8,16 +8,14 @@
ScoDoc 9 API : accès aux moduleimpl ScoDoc 9 API : accès aux moduleimpl
""" """
from flask import g
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import login_required
import app
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import ( from app.models import ModuleImpl
FormSemestre, from app.scodoc import sco_liste_notes
ModuleImpl,
)
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -62,10 +60,7 @@ def moduleimpl(moduleimpl_id: int):
} }
} }
""" """
query = ModuleImpl.query.filter_by(id=moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return modimpl.to_dict(convert_objects=True) return modimpl.to_dict(convert_objects=True)
@ -87,8 +82,36 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
... ...
] ]
""" """
query = ModuleImpl.query.filter_by(id=moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return [i.to_dict() for i in modimpl.inscriptions] return [i.to_dict() for i in modimpl.inscriptions]
@bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def moduleimpl_notes(moduleimpl_id: int):
"""Liste des notes dans ce moduleimpl
Exemple de résultat :
[
{
"etudid": 17776, // code de l'étudiant
"nom": "DUPONT",
"prenom": "Luz",
"38411": 16.0, // Note dans l'évaluation d'id 38411
"38410": 15.0,
"moymod": 15.5, // Moyenne INDICATIVE module
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
},
...
]
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
app.set_sco_dept(modimpl.formsemestre.departement.acronym)
table, _ = sco_liste_notes.do_evaluation_listenotes(
moduleimpl_id=modimpl.id, fmt="json"
)
return table

View File

@ -7,7 +7,6 @@
""" """
ScoDoc 9 API : accès aux utilisateurs ScoDoc 9 API : accès aux utilisateurs
""" """
import datetime
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
@ -15,13 +14,14 @@ from flask_login import current_user, login_required
from app import db, log from app import db, log
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
from app.scodoc.sco_utils import json_error
from app.auth.models import User, Role, UserRole from app.auth.models import User, Role, UserRole
from app.auth.models import is_valid_password from app.auth.models import is_valid_password
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import Departement from app.models import Departement, ScoDocSiteConfig
from app.scodoc import sco_edt_cal
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -441,3 +441,63 @@ def role_delete(role_name: str):
db.session.delete(role) db.session.delete(role)
db.session.commit() db.session.commit()
return {"OK": True} return {"OK": True}
# @bp.route("/user/<int:uid>/edt")
# @api_web_bp.route("/user/<int:uid>/edt")
# @login_required
# @scodoc
# @permission_required(Permission.ScoView)
# @as_json
# def user_edt(uid: int):
# """L'emploi du temps de l'utilisateur.
# Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
# show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
# Il faut la permission ScoView + (UsersView ou bien être connecté comme l'utilisateur demandé)
# """
# if g.scodoc_dept is None: # route API non départementale
# if not current_user.has_permission(Permission.UsersView):
# return scu.json_error(403, "accès non autorisé")
# user: User = db.session.get(User, uid)
# if user is None:
# return json_error(404, "user not found")
# # Check permission
# if current_user.id != user.id:
# if g.scodoc_dept:
# allowed_depts = current_user.get_depts_with_permission(Permission.UsersView)
# if (None not in allowed_depts) and (user.dept not in allowed_depts):
# return json_error(404, "user not found")
# show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
# # Cherche ics
# if not user.edt_id:
# return json_error(404, "user not configured")
# ics_filename = sco_edt_cal.get_ics_user_edt_filename(user.edt_id)
# if not ics_filename:
# return json_error(404, "no calendar for this user")
# _, calendar = sco_edt_cal.load_calendar(ics_filename)
# # TODO:
# # - Construire mapping edt2modimpl: edt_id -> modimpl
# # pour cela, considérer tous les formsemestres de la période de l'edt
# # (soit on considère l'année scolaire du 1er event, ou celle courante,
# # soit on cherche min, max des dates des events)
# # - Modifier décodage des groupes dans convert_ics pour avoi run mapping
# # de groupe par semestre (retrouvé grâce au modimpl associé à l'event)
# raise NotImplementedError() # TODO XXX WIP
# events_scodoc, _ = sco_edt_cal.convert_ics(
# calendar,
# edt2group=edt2group,
# default_group=default_group,
# edt2modimpl=edt2modimpl,
# )
# edt_dict = sco_edt_cal.translate_calendar(
# events_scodoc, group_ids, show_modules_titles=show_modules_titles
# )
# return edt_dict

View File

@ -499,10 +499,8 @@ class BulletinBUT:
d["etud"]["etat_civil"] = etud.etat_civil d["etud"]["etat_civil"] = etud.etat_civil
d.update(self.res.sem) d.update(self.res.sem)
etud_etat = self.res.get_etud_etat(etud.id) etud_etat = self.res.get_etud_etat(etud.id)
d["filigranne"] = sco_bulletins_pdf.get_filigranne( d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
etud_etat, etud_etat, self.prefs, etud.id, res=self.res
self.prefs,
decision_sem=d["semestre"].get("decision"),
) )
if etud_etat == scu.DEMISSION: if etud_etat == scu.DEMISSION:
d["demission"] = "(Démission)" d["demission"] = "(Démission)"

View File

@ -35,6 +35,7 @@ from app.decorators import (
permission_required, permission_required,
) )
from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import FormSemestre, FormSemestreInscription, Identite
from app.scodoc import sco_bulletins_pdf
from app.scodoc.codes_cursus import UE_STANDARD from app.scodoc.codes_cursus import UE_STANDARD
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
@ -104,8 +105,10 @@ def _build_bulletin_but_infos(
bulletins_sem = BulletinBUT(formsemestre) bulletins_sem = BulletinBUT(formsemestre)
if fmt == "pdf": if fmt == "pdf":
bul: dict = bulletins_sem.bulletin_etud_complet(etud) bul: dict = bulletins_sem.bulletin_etud_complet(etud)
filigranne = bul["filigranne"]
else: # la même chose avec un peu moins d'infos else: # la même chose avec un peu moins d'infos
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True) bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
filigranne = ""
decision_ues = ( decision_ues = (
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]} {x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
if "semestre" in bul and "decision_ue" in bul["semestre"] if "semestre" in bul and "decision_ue" in bul["semestre"]
@ -131,6 +134,7 @@ def _build_bulletin_but_infos(
"decision_ues": decision_ues, "decision_ues": decision_ues,
"ects_total": ects_total, "ects_total": ects_total,
"etud": etud, "etud": etud,
"filigranne": filigranne,
"formsemestre": formsemestre, "formsemestre": formsemestre,
"logo": logo, "logo": logo,
"prefs": bulletins_sem.prefs, "prefs": bulletins_sem.prefs,

View File

@ -48,6 +48,7 @@ def make_bulletin_but_court_pdf(
ects_total: float = 0.0, ects_total: float = 0.0,
etud: Identite = None, etud: Identite = None,
formsemestre: FormSemestre = None, formsemestre: FormSemestre = None,
filigranne=""
logo: Logo = None, logo: Logo = None,
prefs: SemPreferences = None, prefs: SemPreferences = None,
title: str = "", title: str = "",
@ -86,6 +87,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
decision_ues: dict = None, decision_ues: dict = None,
ects_total: float = 0.0, ects_total: float = 0.0,
etud: Identite = None, etud: Identite = None,
filigranne="",
formsemestre: FormSemestre = None, formsemestre: FormSemestre = None,
logo: Logo = None, logo: Logo = None,
prefs: SemPreferences = None, prefs: SemPreferences = None,
@ -95,7 +97,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
] = None, ] = None,
ues_acronyms: list[str] = None, ues_acronyms: list[str] = None,
): ):
super().__init__(bul, authuser=current_user) super().__init__(bul, authuser=current_user, filigranne=filigranne)
self.bul = bul self.bul = bul
self.cursus = cursus self.cursus = cursus
self.decision_ues = decision_ues self.decision_ues = decision_ues

View File

@ -380,14 +380,24 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ADJ, sco_codes.ADJ,
] + self.codes ] + self.codes
explanation += f" et {self.nb_rcues_under_8} < 8" explanation += f" et {self.nb_rcues_under_8} < 8"
else: else: # autres cas: non admis, non passage, non dem, pas la moitié des rcue:
self.codes = [ if formsemestre.semestre_id % 2 and self.formsemestre_pair is None:
sco_codes.RED, # Si jury sur un seul semestre impair, ne propose pas redoublement
sco_codes.NAR, # et efface décision éventuellement existante
sco_codes.PAS1NCI, codes = [None]
sco_codes.ADJ, else:
sco_codes.PASD, # voir #488 (discutable, conventions locales) codes = []
] + self.codes self.codes = (
codes
+ [
sco_codes.RED,
sco_codes.NAR,
sco_codes.PAS1NCI,
sco_codes.ADJ,
sco_codes.PASD, # voir #488 (discutable, conventions locales)
]
+ self.codes
)
explanation += f""" et {self.nb_rcues_under_8 explanation += f""" et {self.nb_rcues_under_8
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" } niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
@ -514,7 +524,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF) """Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
du niveau auquel appartient formsemestre. du niveau auquel appartient formsemestre.
-> S_impair, S_pair -> S_impair, S_pair (de la même année scolaire)
Si l'origine est impair, S_impair est l'origine et S_pair est None Si l'origine est impair, S_impair est l'origine et S_pair est None
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
@ -524,9 +534,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
return None, None return None, None
if formsemestre.semestre_id % 2: if formsemestre.semestre_id % 2:
idx_autre = formsemestre.semestre_id + 1 idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
else: else:
idx_autre = formsemestre.semestre_id - 1 idx_autre = formsemestre.semestre_id - 1 # pair: autre = précédent
# Cherche l'autre semestre de la même année scolaire: # Cherche l'autre semestre de la même année scolaire:
autre_formsemestre = None autre_formsemestre = None
@ -610,6 +620,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def next_semestre_ids(self, code: str) -> set[int]: def next_semestre_ids(self, code: str) -> set[int]:
"""Les indices des semestres dans lequels l'étudiant est autorisé """Les indices des semestres dans lequels l'étudiant est autorisé
à poursuivre après le semestre courant. à poursuivre après le semestre courant.
code: code jury sur année BUT
""" """
# La poursuite d'études dans un semestre pair d'une même année # La poursuite d'études dans un semestre pair d'une même année
# est de droit pour tout étudiant. # est de droit pour tout étudiant.
@ -653,6 +664,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
Si les code_rcue et le code_annee ne sont pas fournis, Si les code_rcue et le code_annee ne sont pas fournis,
et qu'il n'y en a pas déjà, enregistre ceux par défaut. et qu'il n'y en a pas déjà, enregistre ceux par défaut.
Si le code_annee est None, efface le code déjà enregistré.
""" """
log("jury_but.DecisionsProposeesAnnee.record_form") log("jury_but.DecisionsProposeesAnnee.record_form")
code_annee = self.codes[0] # si pas dans le form, valeur par defaut code_annee = self.codes[0] # si pas dans le form, valeur par defaut
@ -697,6 +710,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def record(self, code: str, mark_recorded: bool = True) -> bool: def record(self, code: str, mark_recorded: bool = True) -> bool:
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription. """Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
Si l'étudiant est DEM ou DEF, ne fait rien. Si l'étudiant est DEM ou DEF, ne fait rien.
Si le code est None, efface le code déjà enregistré.
Si mark_recorded est vrai, positionne self.recorded Si mark_recorded est vrai, positionne self.recorded
""" """
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
@ -746,7 +760,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
return True return True
def record_autorisation_inscription(self, code: str): def record_autorisation_inscription(self, code: str):
"""Autorisation d'inscription dans semestre suivant""" """Autorisation d'inscription dans semestre suivant.
code: code jury sur année BUT
"""
if self.autorisations_recorded: if self.autorisations_recorded:
return return
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:

View File

@ -154,7 +154,7 @@ def pvjury_table_but(
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"', "_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"', "_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_target": url_for( "_nom_target": url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud.id, etudid=etud.id,
), ),

View File

@ -447,7 +447,7 @@ def jury_but_semestriel(
<div class="nom_etud">{etud.nomprenom}</div> <div class="nom_etud">{etud.nomprenom}</div>
</div> </div>
<div class="bull_photo"><a href="{ <div class="bull_photo"><a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a> }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div> </div>
</div> </div>

View File

@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
raise ScoValueError( raise ScoValueError(
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme} f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
impossible à déterminer pour l'étudiant <a href="{ impossible à déterminer pour l'étudiant <a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}" class="discretelink">{etud.nom_disp()}</a></p> }" class="discretelink">{etud.nom_disp()}</a></p>
<p>Il faut <a href="{ <p>Il faut <a href="{
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept, url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,

View File

@ -32,6 +32,7 @@ Formulaire ajout d'un justificatif sur un étudiant
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import MultipleFileField from flask_wtf.file import MultipleFileField
from wtforms import ( from wtforms import (
BooleanField,
SelectField, SelectField,
StringField, StringField,
SubmitField, SubmitField,
@ -136,6 +137,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
"Module", "Module",
choices={}, # will be populated dynamically choices={}, # will be populated dynamically
) )
est_just = BooleanField("Justifiée")
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm): class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):

View File

@ -34,52 +34,11 @@ import re
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import DecimalField, SubmitField, ValidationError from wtforms import DecimalField, SubmitField, ValidationError
from wtforms.fields.simple import StringField from wtforms.fields.simple import StringField
from wtforms.validators import Optional from wtforms.validators import Optional, Length
from wtforms.widgets import TimeInput from wtforms.widgets import TimeInput
class TimeField(StringField):
"""HTML5 time input.
tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f
"""
widget = TimeInput()
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
super(TimeField, self).__init__(label, validators, **kwargs)
self.fmt = fmt
self.data = None
def _value(self):
if self.raw_data:
return " ".join(self.raw_data)
if self.data and isinstance(self.data, str):
self.data = datetime.time(*map(int, self.data.split(":")))
return self.data and self.data.strftime(self.fmt) or ""
def process_formdata(self, valuelist):
if valuelist:
time_str = " ".join(valuelist)
try:
components = time_str.split(":")
hour = 0
minutes = 0
seconds = 0
if len(components) in range(2, 4):
hour = int(components[0])
minutes = int(components[1])
if len(components) == 3:
seconds = int(components[2])
else:
raise ValueError
self.data = datetime.time(hour, minutes, seconds)
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid time string")) from exc
def check_tick_time(form, field): def check_tick_time(form, field):
"""Le tick_time doit être entre 0 et 60 minutes""" """Le tick_time doit être entre 0 et 60 minutes"""
if field.data < 1 or field.data > 59: if field.data < 1 or field.data > 59:
@ -118,14 +77,36 @@ def check_ics_regexp(form, field):
class ConfigAssiduitesForm(FlaskForm): class ConfigAssiduitesForm(FlaskForm):
"Formulaire paramétrage Module Assiduité" "Formulaire paramétrage Module Assiduité"
assi_morning_time = StringField(
assi_morning_time = TimeField( "Début de la journée",
"Début de la journée" default="",
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm validators=[Length(max=5)],
assi_lunch_time = TimeField( render_kw={
"Heure de midi (date pivot entre matin et après-midi)" "class": "timepicker",
) # TODO "size": 5,
assi_afternoon_time = TimeField("Fin de la journée") # TODO "id": "assi_morning_time",
},
)
assi_lunch_time = StringField(
"Heure de midi (date pivot entre matin et après-midi)",
default="",
validators=[Length(max=5)],
render_kw={
"class": "timepicker",
"size": 5,
"id": "assi_lunch_time",
},
)
assi_afternoon_time = StringField(
"Fin de la journée",
validators=[Length(max=5)],
default="",
render_kw={
"class": "timepicker",
"size": 5,
"id": "assi_afternoon_time",
},
)
assi_tick_time = DecimalField( assi_tick_time = DecimalField(
"Granularité de la timeline (temps en minutes)", "Granularité de la timeline (temps en minutes)",

View File

@ -0,0 +1,49 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""
Formulaire configuration RGPD
"""
from flask_wtf import FlaskForm
from wtforms import SubmitField
from wtforms.fields.simple import TextAreaField
class ConfigRGPDForm(FlaskForm):
"Formulaire paramétrage RGPD"
rgpd_coordonnees_dpo = TextAreaField(
label="Optionnel: coordonnées du DPO",
description="""Le délégué à la protection des données (DPO) est chargé de mettre en œuvre
la conformité au règlement européen sur la protection des données (RGPD) au sein de lorganisme.
Indiquer ici les coordonnées (format libre) qui seront affichées aux utilisateurs de ScoDoc.
""",
render_kw={"rows": 5, "cols": 72},
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -2,6 +2,7 @@
"""Gestion de l'assiduité (assiduités + justificatifs) """Gestion de l'assiduité (assiduités + justificatifs)
""" """
from datetime import datetime from datetime import datetime
from flask_login import current_user from flask_login import current_user
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from sqlalchemy.exc import DataError from sqlalchemy.exc import DataError
@ -88,8 +89,10 @@ class Assiduite(ScoDocModel):
lazy="select", lazy="select",
) )
def to_dict(self, format_api=True) -> dict: def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
"""Retourne la représentation json de l'assiduité""" """Retourne la représentation json de l'assiduité
restrict n'est pas utilisé ici.
"""
etat = self.etat etat = self.etat
user: User | None = None user: User | None = None
if format_api: if format_api:
@ -252,43 +255,19 @@ class Assiduite(ScoDocModel):
def set_moduleimpl(self, moduleimpl_id: int | str): def set_moduleimpl(self, moduleimpl_id: int | str):
"""Mise à jour du moduleimpl_id """Mise à jour du moduleimpl_id
Les valeurs du champs "moduleimpl_id" possibles sont : Les valeurs du champ "moduleimpl_id" possibles sont :
- <int> (un id classique) - <int> (un id classique)
- <str> ("autre" ou "<id>") - <str> ("autre" ou "<id>")
- None (pas de moduleimpl_id) - "" (pas de moduleimpl_id)
Si la valeur est "autre" il faut: Si la valeur est "autre" il faut:
- mettre à None assiduité.moduleimpl_id - mettre à None assiduité.moduleimpl_id
- mettre à jour assiduite.external_data["module"] = "autre" - mettre à jour assiduite.external_data["module"] = "autre"
En fonction de la configuration du semestre la valeur `None` peut-être considérée comme invalide. En fonction de la configuration du semestre (option force_module) la valeur "" peut-être
considérée comme invalide.
- Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité - Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité
""" """
moduleimpl: ModuleImpl = None moduleimpl: ModuleImpl = None
try: if moduleimpl_id == "autre":
# ne lève une erreur que si moduleimpl_id est une chaine de caractère non parsable (parseInt)
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
# moduleImpl est soit :
# - None si moduleimpl_id==None
# - None si moduleimpl_id==<int> non reconnu
# - ModuleImpl si <int|str> valide
# Vérification ModuleImpl not None (raise ScoValueError)
if moduleimpl is None and self._check_force_module(moduleimpl):
# Ici uniquement si on est autorisé à ne pas avoir de module
self.moduleimpl_id = None
return
# Vérification Inscription ModuleImpl (raise ScoValueError)
if moduleimpl.est_inscrit(self.etudiant):
self.moduleimpl_id = moduleimpl.id
else:
raise ScoValueError("L'étudiant n'est pas inscrit au module")
except DataError:
# On arrive ici si moduleimpl_id == "autre" ou moduleimpl_id == <str> non parsé
if moduleimpl_id != "autre":
raise ScoValueError("Module non reconnu")
# Configuration de external_data pour Module Autre # Configuration de external_data pour Module Autre
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"} # Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
# Sinon on met à jour external_data["module"] à "autre" # Sinon on met à jour external_data["module"] à "autre"
@ -302,6 +281,29 @@ class Assiduite(ScoDocModel):
self.moduleimpl_id = None self.moduleimpl_id = None
# Ici pas de vérification du force module car on l'a mis dans "external_data" # Ici pas de vérification du force module car on l'a mis dans "external_data"
return
if moduleimpl_id != "":
try:
moduleimpl_id = int(moduleimpl_id)
except ValueError as exc:
raise ScoValueError("Module non reconnu") from exc
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
# ici moduleimpl est None si non spécifié
# Vérification ModuleImpl not None (raise ScoValueError)
if moduleimpl is None:
self._check_force_module()
# Ici uniquement si on est autorisé à ne pas avoir de module
self.moduleimpl_id = None
return
# Vérification Inscription ModuleImpl (raise ScoValueError)
if moduleimpl.est_inscrit(self.etudiant):
self.moduleimpl_id = moduleimpl.id
else:
raise ScoValueError("L'étudiant n'est pas inscrit au module")
def supprime(self): def supprime(self):
"Supprime l'assiduité. Log et commit." "Supprime l'assiduité. Log et commit."
@ -331,7 +333,7 @@ class Assiduite(ScoDocModel):
return get_formsemestre_from_data(self.to_dict()) return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str: def get_module(self, traduire: bool = False) -> int | str:
"TODO" "TODO documenter"
if self.moduleimpl_id is not None: if self.moduleimpl_id is not None:
if traduire: if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
@ -360,8 +362,12 @@ class Assiduite(ScoDocModel):
return f"saisie le {date} {utilisateur}" return f"saisie le {date} {utilisateur}"
def _check_force_module(self, moduleimpl: ModuleImpl) -> bool: def _check_force_module(self):
# Vérification si module forcé """Vérification si module forcé:
Si le module est requis, raise ScoValueError
sinon ne fait rien.
"""
# cherche le formsemestre affecté pour utiliser ses préférences
formsemestre: FormSemestre = get_formsemestre_from_data( formsemestre: FormSemestre = get_formsemestre_from_data(
{ {
"etudid": self.etudid, "etudid": self.etudid,
@ -369,18 +375,15 @@ class Assiduite(ScoDocModel):
"date_fin": self.date_fin, "date_fin": self.date_fin,
} }
) )
force: bool formsemestre_id = formsemestre.id if formsemestre else None
# si pas de formsemestre, utilisera les prefs globales du département
if formsemestre: dept_id = self.etudiant.dept_id
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id) force = is_assiduites_module_forced(
else: formsemestre_id=formsemestre_id, dept_id=dept_id
force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id) )
if force: if force:
raise ScoValueError("Module non renseigné") raise ScoValueError("Module non renseigné")
return True
class Justificatif(ScoDocModel): class Justificatif(ScoDocModel):
""" """
@ -434,6 +437,14 @@ class Justificatif(ScoDocModel):
etudiant = db.relationship( etudiant = db.relationship(
"Identite", back_populates="justificatifs", lazy="joined" "Identite", back_populates="justificatifs", lazy="joined"
) )
# En revanche, user est rarement accédé:
user = db.relationship(
"User",
backref=db.backref(
"justificatifs", lazy="select", order_by="Justificatif.entry_date"
),
lazy="select",
)
external_data = db.Column(db.JSON, nullable=True) external_data = db.Column(db.JSON, nullable=True)
@ -445,20 +456,16 @@ class Justificatif(ScoDocModel):
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
return query.first_or_404() return query.first_or_404()
def to_dict(self, format_api: bool = False) -> dict: def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
"""transformation de l'objet en dictionnaire sérialisable""" """L'objet en dictionnaire sérialisable.
Si restrict, ne donne par la raison et les fichiers et external_data
"""
etat = self.etat etat = self.etat
username = self.user_id user: User = self.user if self.user_id is not None else None
if format_api: if format_api:
etat = EtatJustificatif.inverse().get(self.etat).name etat = EtatJustificatif.inverse().get(self.etat).name
if self.user_id is not None:
user: User = db.session.get(User, self.user_id)
if user is None:
username = "Non renseigné"
else:
username = user.get_prenomnom()
data = { data = {
"justif_id": self.justif_id, "justif_id": self.justif_id,
@ -467,11 +474,13 @@ class Justificatif(ScoDocModel):
"date_debut": self.date_debut, "date_debut": self.date_debut,
"date_fin": self.date_fin, "date_fin": self.date_fin,
"etat": etat, "etat": etat,
"raison": self.raison, "raison": None if restrict else self.raison,
"fichier": self.fichier, "fichier": None if restrict else self.fichier,
"entry_date": self.entry_date, "entry_date": self.entry_date,
"user_id": username, "user_id": None if user is None else user.id, # l'uid
"external_data": self.external_data, "user_name": None if user is None else user.user_name, # le login
"user_nom_complet": None if user is None else user.get_nomcomplet(),
"external_data": None if restrict else self.external_data,
} }
return data return data
@ -618,6 +627,12 @@ def compute_assiduites_justified(
Returns: Returns:
list[int]: la liste des assiduités qui ont été justifiées. list[int]: la liste des assiduités qui ont été justifiées.
""" """
# TODO à optimiser (car très long avec 40000 assiduités)
# On devrait :
# - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés
# - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie
# - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant # Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
if justificatifs is None: if justificatifs is None:
justificatifs: list[Justificatif] = Justificatif.query.filter_by( justificatifs: list[Justificatif] = Justificatif.query.filter_by(

View File

@ -119,6 +119,9 @@ class Identite(models.ScoDocModel):
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete" "Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
) )
# Champs "protégés" par ViewEtudData (RGPD)
protected_attrs = {"boursier"}
def __repr__(self): def __repr__(self):
return ( return (
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>" f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
@ -176,7 +179,7 @@ class Identite(models.ScoDocModel):
def url_fiche(self) -> str: def url_fiche(self) -> str:
"url de la fiche étudiant" "url de la fiche étudiant"
return url_for( return url_for(
"scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id "scolar.fiche_etud", scodoc_dept=self.departement.acronym, etudid=self.id
) )
@classmethod @classmethod
@ -418,7 +421,7 @@ class Identite(models.ScoDocModel):
return args_dict return args_dict
def to_dict_short(self) -> dict: def to_dict_short(self) -> dict:
"""Les champs essentiels""" """Les champs essentiels (aucune donnée perso protégée)"""
return { return {
"id": self.id, "id": self.id,
"civilite": self.civilite, "civilite": self.civilite,
@ -433,9 +436,10 @@ class Identite(models.ScoDocModel):
"prenom_etat_civil": self.prenom_etat_civil, "prenom_etat_civil": self.prenom_etat_civil,
} }
def to_dict_scodoc7(self) -> dict: def to_dict_scodoc7(self, restrict=False) -> dict:
"""Représentation dictionnaire, """Représentation dictionnaire,
compatible ScoDoc7 mais sans infos admission compatible ScoDoc7 mais sans infos admission.
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
""" """
e_dict = self.__dict__.copy() # dict(self.__dict__) e_dict = self.__dict__.copy() # dict(self.__dict__)
e_dict.pop("_sa_instance_state", None) e_dict.pop("_sa_instance_state", None)
@ -446,7 +450,7 @@ class Identite(models.ScoDocModel):
e_dict["nomprenom"] = self.nomprenom e_dict["nomprenom"] = self.nomprenom
adresse = self.adresses.first() adresse = self.adresses.first()
if adresse: if adresse:
e_dict.update(adresse.to_dict()) e_dict.update(adresse.to_dict(restrict=restrict))
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
def to_dict_bul(self, include_urls=True): def to_dict_bul(self, include_urls=True):
@ -481,7 +485,7 @@ class Identite(models.ScoDocModel):
if include_urls and has_request_context(): if include_urls and has_request_context():
# test request context so we can use this func in tests under the flask shell # test request context so we can use this func in tests under the flask shell
d["fiche_url"] = url_for( d["fiche_url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=self.id
) )
d["photo_url"] = sco_photos.get_etud_photo_url(self.id) d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
adresse = self.adresses.first() adresse = self.adresses.first()
@ -490,16 +494,22 @@ class Identite(models.ScoDocModel):
d["id"] = self.id # a été écrasé par l'id de adresse d["id"] = self.id # a été écrasé par l'id de adresse
return d return d
def to_dict_api(self) -> dict: def to_dict_api(self, restrict=False) -> dict:
"""Représentation dictionnaire pour export API, avec adresses et admission.""" """Représentation dictionnaire pour export API, avec adresses et admission.
Si restrict, supprime les infos "personnelles" (boursier)
"""
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
admission = self.admission admission = self.admission
e["admission"] = admission.to_dict() if admission is not None else None e["admission"] = admission.to_dict() if admission is not None else None
e["adresses"] = [adr.to_dict() for adr in self.adresses] e["adresses"] = [adr.to_dict(restrict=restrict) for adr in self.adresses]
e["dept_acronym"] = self.departement.acronym e["dept_acronym"] = self.departement.acronym
e.pop("departement", None) e.pop("departement", None)
e["sort_key"] = self.sort_key e["sort_key"] = self.sort_key
if restrict:
# Met à None les attributs protégés:
for attr in self.protected_attrs:
e[attr] = None
return e return e
def inscriptions(self) -> list["FormSemestreInscription"]: def inscriptions(self) -> list["FormSemestreInscription"]:
@ -825,12 +835,25 @@ class Adresse(models.ScoDocModel):
) )
description = db.Column(db.Text) description = db.Column(db.Text)
def to_dict(self, convert_nulls_to_str=False): # Champs "protégés" par ViewEtudData (RGPD)
"""Représentation dictionnaire,""" protected_attrs = {
"emailperso",
"domicile",
"codepostaldomicile",
"villedomicile",
"telephone",
"telephonemobile",
"fax",
}
def to_dict(self, convert_nulls_to_str=False, restrict=False):
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
if convert_nulls_to_str: if convert_nulls_to_str:
return {k: e[k] or "" for k in e} e = {k: v or "" for k, v in e.items()}
if restrict:
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
return e return e
@ -885,12 +908,16 @@ class Admission(models.ScoDocModel):
# classement (1..Ngr) par le jury dans le groupe APB # classement (1..Ngr) par le jury dans le groupe APB
apb_classement_gr = db.Column(db.Integer) apb_classement_gr = db.Column(db.Integer)
# Tous les champs sont "protégés" par ViewEtudData (RGPD)
# sauf:
not_protected_attrs = {"bac", "specialite", "anne_bac"}
def get_bac(self) -> Baccalaureat: def get_bac(self) -> Baccalaureat:
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères." "Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
return Baccalaureat(self.bac, specialite=self.specialite) return Baccalaureat(self.bac, specialite=self.specialite)
def to_dict(self, no_nulls=False): def to_dict(self, no_nulls=False, restrict=False):
"""Représentation dictionnaire,""" """Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
d = dict(self.__dict__) d = dict(self.__dict__)
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
if no_nulls: if no_nulls:
@ -905,6 +932,8 @@ class Admission(models.ScoDocModel):
d[key] = 0 d[key] = 0
elif isinstance(col_type, sqlalchemy.Boolean): elif isinstance(col_type, sqlalchemy.Boolean):
d[key] = False d[key] = False
if restrict:
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
return d return d
@classmethod @classmethod

View File

@ -184,7 +184,7 @@ class Evaluation(db.Model):
# ScoDoc7 output_formators # ScoDoc7 output_formators
e_dict["evaluation_id"] = self.id e_dict["evaluation_id"] = self.id
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None e_dict["date_fin"] = self.date_fin.isoformat() if self.date_fin else None
e_dict["numero"] = self.numero or 0 e_dict["numero"] = self.numero or 0
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids } e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
@ -428,8 +428,8 @@ class Evaluation(db.Model):
def get_ue_poids_str(self) -> str: def get_ue_poids_str(self) -> str:
"""string describing poids, for excel cells and pdfs """string describing poids, for excel cells and pdfs
Note: si les poids ne sont pas initialisés (poids par défaut), Note: les poids nuls ou non initialisés (poids par défaut),
ils ne sont pas affichés. ne sont pas affichés.
""" """
# restreint aux UE du semestre dans lequel est cette évaluation # restreint aux UE du semestre dans lequel est cette évaluation
# au cas où le module ait changé de semestre et qu'il reste des poids # au cas où le module ait changé de semestre et qu'il reste des poids
@ -440,7 +440,7 @@ class Evaluation(db.Model):
for p in sorted( for p in sorted(
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme) self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
) )
if evaluation_semestre_idx == p.ue.semestre_idx if evaluation_semestre_idx == p.ue.semestre_idx and (p.poids or 0) > 0
] ]
) )
@ -584,20 +584,10 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
if date_debut and date_fin: if date_debut and date_fin:
duration = data["date_fin"] - data["date_debut"] duration = data["date_fin"] - data["date_debut"]
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION: if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
raise ScoValueError("Heures de l'évaluation incohérentes !") raise ScoValueError(
# # --- heures "Heures de l'évaluation incohérentes !",
# heure_debut = data.get("heure_debut", None) dest_url="javascript:history.back();",
# if heure_debut and not isinstance(heure_debut, datetime.time): )
# if date_format == "dmy":
# data["heure_debut"] = heure_to_time(heure_debut)
# else: # ISO
# data["heure_debut"] = datetime.time.fromisoformat(heure_debut)
# heure_fin = data.get("heure_fin", None)
# if heure_fin and not isinstance(heure_fin, datetime.time):
# if date_format == "dmy":
# data["heure_fin"] = heure_to_time(heure_fin)
# else: # ISO
# data["heure_fin"] = datetime.time.fromisoformat(heure_fin)
def heure_to_time(heure: str) -> datetime.time: def heure_to_time(heure: str) -> datetime.time:

View File

@ -187,7 +187,7 @@ class FormSemestre(db.Model):
def get_formsemestre( def get_formsemestre(
cls, formsemestre_id: int | str, dept_id: int = None cls, formsemestre_id: int | str, dept_id: int = None
) -> "FormSemestre": ) -> "FormSemestre":
""" "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant""" """FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls """ScoDoc models: moduleimpls
""" """
import pandas as pd import pandas as pd
from flask import abort, g
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from app import db from app import db
@ -82,6 +83,23 @@ class ModuleImpl(db.Model):
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids) df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
return evaluations_poids return evaluations_poids
@classmethod
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant."""
from app.models.formsemestre import FormSemestre
if not isinstance(moduleimpl_id, int):
try:
moduleimpl_id = int(moduleimpl_id)
except (TypeError, ValueError):
abort(404, "moduleimpl_id invalide")
if g.scodoc_dept:
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
query = cls.query.filter_by(id=moduleimpl_id)
if dept_id is not None:
query = query.join(FormSemestre).filter_by(dept_id=dept_id)
return query.first_or_404()
def invalidate_evaluations_poids(self): def invalidate_evaluations_poids(self):
"""Invalide poids cachés""" """Invalide poids cachés"""
df_cache.EvaluationsPoidsCache.delete(self.id) df_cache.EvaluationsPoidsCache.delete(self.id)

1274
app/pe/pe_jurype.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -685,6 +685,11 @@ class TF(object):
'<input type="text" name="%s" size="10" value="%s" class="datepicker">' '<input type="text" name="%s" size="10" value="%s" class="datepicker">'
% (field, values[field]) % (field, values[field])
) )
elif input_type == "time": # JavaScript widget for date input
lem.append(
f"""<input type="text" name="{field}" maxlength="5" size="5" value="{
values[field]}" class="timepicker">"""
)
elif input_type == "text_suggest": elif input_type == "text_suggest":
lem.append( lem.append(
'<input type="text" name="%s" id="%s" size="%d" %s' '<input type="text" name="%s" id="%s" size="%d" %s'

View File

@ -145,7 +145,9 @@ def sco_header(
etudid=None, etudid=None,
formsemestre_id=None, formsemestre_id=None,
): ):
"Main HTML page header for ScoDoc" """Main HTML page header for ScoDoc
Utilisé dans les anciennes pages. Les nouvelles pages utilisent le template Jinja.
"""
from app.scodoc.sco_formsemestre_status import formsemestre_page_title from app.scodoc.sco_formsemestre_status import formsemestre_page_title
if etudid is not None: if etudid is not None:
@ -189,7 +191,12 @@ def sco_header(
# jQuery UI # jQuery UI
# can modify loaded theme here # can modify loaded theme here
H.append( H.append(
f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n' f"""
<link type="text/css" rel="stylesheet"
href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link type="text/css" rel="stylesheet"
href="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.css" />
"""
) )
if init_google_maps: if init_google_maps:
# It may be necessary to add an API key: # It may be necessary to add an API key:
@ -219,19 +226,26 @@ def sco_header(
# jQuery # jQuery
H.append( H.append(
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script> f"""
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>""" <script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
"""
) )
# qTip # qTip
if init_qtip: if init_qtip:
H.append( H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script> f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />""" <link type="text/css" rel="stylesheet"
href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
"""
) )
H.append( H.append(
f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script> f"""<script
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>""" src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
"""
) )
if init_google_maps: if init_google_maps:
H.append( H.append(

View File

@ -32,12 +32,68 @@ from flask import render_template, url_for
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
from app import db
from app.models import Evaluation, GroupDescr, ModuleImpl, Partition
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION from sco_version import SCOVERSION
def retreive_formsemestre_from_request() -> int:
"""Cherche si on a de quoi déduire le semestre affiché à partir des
arguments de la requête:
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
Returns None si pas défini.
"""
if request.method == "GET":
args = request.args
elif request.method == "POST":
args = request.form
else:
return None
formsemestre_id = None
# Search formsemestre
group_ids = args.get("group_ids", [])
if "formsemestre_id" in args:
formsemestre_id = args["formsemestre_id"]
elif "moduleimpl_id" in args and args["moduleimpl_id"]:
modimpl = db.session.get(ModuleImpl, args["moduleimpl_id"])
if not modimpl:
return None # suppressed ?
formsemestre_id = modimpl.formsemestre_id
elif "evaluation_id" in args:
evaluation = db.session.get(Evaluation, args["evaluation_id"])
if not evaluation:
return None # evaluation suppressed ?
formsemestre_id = evaluation.moduleimpl.formsemestre_id
elif "group_id" in args:
group = db.session.get(GroupDescr, args["group_id"])
if not group:
return None
formsemestre_id = group.partition.formsemestre_id
elif group_ids:
if isinstance(group_ids, str):
group_ids = group_ids.split(",")
group_id = group_ids[0]
group = db.session.get(GroupDescr, group_id)
if not group:
return None
formsemestre_id = group.partition.formsemestre_id
elif "partition_id" in args:
partition = db.session.get(Partition, args["partition_id"])
if not partition:
return None
formsemestre_id = partition.formsemestre_id
if formsemestre_id is None:
return None # no current formsemestre
try:
return int(formsemestre_id)
except ValueError:
return None # no current formsemestre
def sidebar_common(): def sidebar_common():
"partie commune à toutes les sidebar" "partie commune à toutes les sidebar"
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept) home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
@ -107,7 +163,7 @@ def sidebar(etudid: int = None):
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
params.update(etud) params.update(etud)
params["fiche_url"] = url_for( params["fiche_url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
) )
# compte les absences du semestre en cours # compte les absences du semestre en cours
H.append( H.append(
@ -129,13 +185,17 @@ def sidebar(etudid: int = None):
) )
H.append("<ul>") H.append("<ul>")
if current_user.has_permission(Permission.AbsChange): if current_user.has_permission(Permission.AbsChange):
# essaie de conserver le semestre actuellement en vue
cur_formsemestre_id = retreive_formsemestre_from_request()
H.append( H.append(
f""" f"""
<li><a href="{ url_for('assiduites.ajout_assiduite_etud', <li><a href="{ url_for('assiduites.ajout_assiduite_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Ajouter</a></li> }">Ajouter</a></li>
<li><a href="{ url_for('assiduites.ajout_justificatif_etud', <li><a href="{ url_for('assiduites.ajout_justificatif_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=cur_formsemestre_id,
)
}">Justifier</a></li> }">Justifier</a></li>
""" """
) )

View File

@ -129,7 +129,7 @@ def table_billets(
] = f'id="{billet.etudiant.id}" class="etudinfo"' ] = f'id="{billet.etudiant.id}" class="etudinfo"'
if with_links: if with_links:
billet_dict["_nomprenom_target"] = url_for( billet_dict["_nomprenom_target"] = url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=billet_dict["etudid"], etudid=billet_dict["etudid"],
) )

View File

@ -34,7 +34,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence.
import datetime import datetime
from typing import Optional from typing import Optional
from flask import current_app, g, url_for from flask import g, url_for
from flask_mail import Message from flask_mail import Message
from app import db from app import db
@ -42,6 +42,7 @@ from app import email
from app import log from app import log
from app.auth.models import User from app.auth.models import User
from app.models.absences import AbsenceNotification from app.models.absences import AbsenceNotification
from app.models.etudiants import Identite
from app.models.events import Scolog from app.models.events import Scolog
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -108,7 +109,6 @@ def do_abs_notify(
return # abort return # abort
# Vérification fréquence (pour ne pas envoyer de mails trop souvent) # Vérification fréquence (pour ne pas envoyer de mails trop souvent)
# TODO Mettre la fréquence dans les préférences assiduités
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq") abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
destinations_filtered = [] destinations_filtered = []
for email_addr in destinations: for email_addr in destinations:
@ -175,9 +175,15 @@ def abs_notify_get_destinations(
if prefs["abs_notify_email"]: if prefs["abs_notify_email"]:
destinations.append(prefs["abs_notify_email"]) destinations.append(prefs["abs_notify_email"])
if prefs["abs_notify_etud"]: if prefs["abs_notify_etud"]:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = Identite.get_etud(etudid)
if etud["email_default"]: adresse = etud.adresses.first()
destinations.append(etud["email_default"]) if adresse:
# Mail à utiliser pour les envois vers l'étudiant:
# choix qui pourrait être controlé par une preference
# ici priorité au mail institutionnel:
email_default = adresse.email or adresse.emailperso
if email_default:
destinations.append(email_default)
# Notification (à chaque fois) des resp. de modules ayant des évaluations # Notification (à chaque fois) des resp. de modules ayant des évaluations
# à cette date # à cette date
@ -271,7 +277,7 @@ def abs_notification_message(
values["nbabsjust"] = nbabsjust values["nbabsjust"] = nbabsjust
values["nbabsnonjust"] = nbabs - nbabsjust values["nbabsnonjust"] = nbabs - nbabsjust
values["url_ficheetud"] = url_for( values["url_ficheetud"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
) )
template = prefs["abs_notification_mail_tmpl"] template = prefs["abs_notification_mail_tmpl"]

View File

@ -62,7 +62,7 @@ def can_edit_etud_archive(authuser):
def etud_list_archives_html(etud: Identite): def etud_list_archives_html(etud: Identite):
"""HTML snippet listing archives""" """HTML snippet listing archives."""
can_edit = can_edit_etud_archive(current_user) can_edit = can_edit_etud_archive(current_user)
etud_archive_id = etud.id etud_archive_id = etud.id
L = [] L = []
@ -177,7 +177,7 @@ def etud_upload_file_form(etudid):
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
else: else:
data = tf[2]["datafile"].read() data = tf[2]["datafile"].read()
@ -188,7 +188,7 @@ def etud_upload_file_form(etudid):
etud_archive_id, data, filename, description=descr etud_archive_id, data, filename, description=descr
) )
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
@ -228,7 +228,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
), ),
dest_url="", dest_url="",
cancel_url=url_for( cancel_url=url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etudid, etudid=etudid,
head_message="annulation", head_message="annulation",
@ -239,7 +239,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"]) ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"])
flash("Archive supprimée") flash("Archive supprimée")
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )

View File

@ -17,7 +17,15 @@ from app import log
class Trace: class Trace:
"""gestionnaire de la trace des fichiers justificatifs """gestionnaire de la trace des fichiers justificatifs
XXX TODO à documenter: rôle et format des fichier strace
Role des fichiers traces :
- Sauvegarder la date de dépot du fichier
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
_trace.csv :
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
""" """
def __init__(self, path: str) -> None: def __init__(self, path: str) -> None:
@ -39,7 +47,7 @@ class Trace:
continue continue
entry_date: datetime = is_iso_formated(csv[1], True) entry_date: datetime = is_iso_formated(csv[1], True)
delete_date: datetime = is_iso_formated(csv[2], True) delete_date: datetime = is_iso_formated(csv[2], True)
user_id = csv[3] user_id = csv[3].strip()
self.content[fname] = [entry_date, delete_date, user_id] self.content[fname] = [entry_date, delete_date, user_id]
if os.path.isfile(self.path): if os.path.isfile(self.path):
@ -84,7 +92,14 @@ class Trace:
self, fnames: list[str] = None self, fnames: list[str] = None
) -> dict[str, list[datetime, datetime, str]]: ) -> dict[str, list[datetime, datetime, str]]:
"""Récupère la trace pour les noms de fichiers. """Récupère la trace pour les noms de fichiers.
si aucun nom n'est donné, récupère tous les fichiers""" si aucun nom n'est donné, récupère tous les fichiers
retour :
{
"nom_fichier_srv": [datetime_depot, datetime_suppr/None, user_id],
...
}
"""
if fnames is None: if fnames is None:
return self.content return self.content
@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver):
filenames = self.list_archive(archive_id, dept_id=etud.dept_id) filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
trace: Trace = Trace(archive_id) trace: Trace = Trace(archive_id)
traced = trace.get_trace(filenames) traced = trace.get_trace(filenames)
return [(key, value[2]) for key, value in traced.items() if value is not None]
return [(key, value[2]) for key, value in traced.items()]
def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str): def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
""" """

View File

@ -6,7 +6,7 @@ from pytz import UTC
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from app import log, db from app import log, db, set_sco_dept
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
from app.models.etudiants import Identite from app.models.etudiants import Identite
@ -170,7 +170,7 @@ class CountCalculator:
"""Récupère une clé de dictionnaire en fonction de l'état de l'assiduité """Récupère une clé de dictionnaire en fonction de l'état de l'assiduité
et si elle est justifié et si elle est justifié
""" """
keys: dict[EtatAssiduite, str] = { keys: dict[scu.EtatAssiduite, str] = {
scu.EtatAssiduite.ABSENT: "absent", scu.EtatAssiduite.ABSENT: "absent",
scu.EtatAssiduite.RETARD: "retard", scu.EtatAssiduite.RETARD: "retard",
scu.EtatAssiduite.PRESENT: "present", scu.EtatAssiduite.PRESENT: "present",
@ -349,6 +349,11 @@ def get_assiduites_stats(
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
) -> dict[str, int | float]: ) -> dict[str, int | float]:
"""Compte les assiduités en fonction des filtres""" """Compte les assiduités en fonction des filtres"""
# XXX TODO-assiduite : documenter !!!
# Que sont les filtres ? Quelles valeurs ?
# documenter permet de faire moins de bug: qualité du code non satisfaisante.
#
# + on se perd entre les clés en majuscules et en minuscules. Pourquoi
if filtered is not None: if filtered is not None:
deb, fin = None, None deb, fin = None, None
@ -390,17 +395,18 @@ def get_assiduites_stats(
# Récupération des états # Récupération des états
etats: list[str] = ( etats: list[str] = (
filtered["etat"].split(",") filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
if "etat" in filtered
else ["absent", "present", "retard"]
) )
# être sur que les états sont corrects # être sur que les états sont corrects
etats = [etat for etat in etats if etat in ["absent", "present", "retard"]] etats = [etat for etat in etats if etat.upper() in scu.EtatAssiduite.all()]
# Préparation du dictionnaire de retour avec les valeurs du calcul # Préparation du dictionnaire de retour avec les valeurs du calcul
count: dict = calculator.to_dict(only_total=False) count: dict = calculator.to_dict(only_total=False)
for etat in etats: for etat in etats:
# TODO-assiduite: on se perd entre les lower et upper.
# Pourquoi EtatAssiduite est en majuscules si tout le reste est en minuscules ?
etat = etat.lower()
if etat != "present": if etat != "present":
output[etat] = count[etat] output[etat] = count[etat]
output[etat]["justifie"] = count[etat + "_just"] output[etat]["justifie"] = count[etat + "_just"]
@ -452,8 +458,6 @@ def filter_by_date(
if date_fin is None: if date_fin is None:
date_fin = datetime.max date_fin = datetime.max
date_deb = scu.localize_datetime(date_deb) # TODO A modifier (timezone ?)
date_fin = scu.localize_datetime(date_fin)
if not strict: if not strict:
return collection.filter( return collection.filter(
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
@ -560,15 +564,19 @@ def get_all_justified(
return after return after
def create_absence( def create_absence_billet(
date_debut: datetime, date_debut: datetime,
date_fin: datetime, date_fin: datetime,
etudid: int, etudid: int,
description: str = None, description: str = None,
est_just: bool = False, est_just: bool = False,
) -> int: ) -> int:
"""TODO: doc, dire quand l'utiliser""" """
# TODO Permet de rapidement créer une absence.
**UTILISÉ UNIQUEMENT POUR LES BILLETS**
Ne pas utiliser autre par.
TALK: Vérifier si nécessaire
"""
etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404() etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404()
assiduite_unique: Assiduite = Assiduite.create_assiduite( assiduite_unique: Assiduite = Assiduite.create_assiduite(
etud=etud, etud=etud,
@ -650,8 +658,7 @@ def get_assiduites_count_in_interval(
""" """
date_debut_iso = date_debut_iso or date_debut.isoformat() date_debut_iso = date_debut_iso or date_debut.isoformat()
date_fin_iso = date_fin_iso or date_fin.isoformat() date_fin_iso = date_fin_iso or date_fin.isoformat()
# TODO Question: pourquoi ne pas cacher toutes les métriques, si l'API les veut toutes ? key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key) r = sco_cache.AbsSemEtudCache.get(key)
if not r or moduleimpl_id is not None: if not r or moduleimpl_id is not None:
@ -668,26 +675,27 @@ def get_assiduites_count_in_interval(
calculator: CountCalculator = CountCalculator() calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites) calculator.compute_assiduites(assiduites)
calcul: dict = calculator.to_dict(only_total=False) calcul: dict = calculator.to_dict(only_total=False)
nb_abs: dict = calcul["absent"][metrique]
nb_abs_just: dict = calcul["absent_just"][metrique]
r = (nb_abs, nb_abs_just) r = calcul
if moduleimpl_id is None: if moduleimpl_id is None:
ans = sco_cache.AbsSemEtudCache.set(key, r) ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans: if not ans:
log("warning: get_assiduites_count failed to cache") log("warning: get_assiduites_count failed to cache")
return r
nb_abs: dict = r["absent"][metrique]
nb_abs_just: dict = r["absent_just"][metrique]
return (nb_abs, nb_abs_just)
def invalidate_assiduites_count(etudid: int, sem: dict): def invalidate_assiduites_count(etudid: int, sem: dict):
"""Invalidate (clear) cached counts""" """Invalidate (clear) cached counts"""
date_debut = sem["date_debut_iso"] date_debut = sem["date_debut_iso"]
date_fin = sem["date_fin_iso"] date_fin = sem["date_fin_iso"]
for met in scu.AssiduitesMetrics.TAG: key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites"
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites" sco_cache.AbsSemEtudCache.delete(key)
sco_cache.AbsSemEtudCache.delete(key)
# Non utilisé
def invalidate_assiduites_count_sem(sem: dict): def invalidate_assiduites_count_sem(sem: dict):
"""Invalidate (clear) cached abs counts for all the students of this semestre""" """Invalidate (clear) cached abs counts for all the students of this semestre"""
inscriptions = ( inscriptions = (
@ -756,3 +764,14 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
etudid = etudid if etudid is not None else obj["etudid"] etudid = etudid if etudid is not None else obj["etudid"]
invalidate_assiduites_etud_date(etudid, date_debut) invalidate_assiduites_etud_date(etudid, date_debut)
invalidate_assiduites_etud_date(etudid, date_fin) invalidate_assiduites_etud_date(etudid, date_fin)
# mettre à jour le scodoc_dept en fonction de l'étudiant
etud = Identite.query.filter_by(etudid=etudid).first_or_404()
set_sco_dept(etud.departement.acronym)
# Invalide les caches des tableaux de l'étudiant
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(
pattern=f"tableau-etud-{etudid}*"
)
# Invalide les tableaux "bilan dept"
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")

View File

@ -60,6 +60,7 @@ import traceback
from flask import g, request from flask import g, request
from app import log, ScoValueError from app import log, ScoValueError
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
@ -318,14 +319,34 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
return pdfdoc, filename return pdfdoc, filename
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str: def get_filigranne(
"""Texte à placer en "filigranne" sur le bulletin pdf""" etud_etat: str, prefs, decision_sem: str | None | bool = None
) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf.
etud_etat : etat de l'inscription (I ou D)
decision_sem = code jury ou vide
"""
if etud_etat == scu.DEMISSION: if etud_etat == scu.DEMISSION:
return "Démission" return "Démission"
elif etud_etat == codes_cursus.DEF: if etud_etat == codes_cursus.DEF:
return "Défaillant" return "Défaillant"
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[ if (prefs["bul_show_temporary"] and not decision_sem) or prefs[
"bul_show_temporary_forced" "bul_show_temporary_forced"
]: ]:
return prefs["bul_temporary_txt"] return prefs["bul_temporary_txt"]
return "" return ""
def get_filigranne_apc(
etud_etat: str, prefs, etudid: int, res: ResultatsSemestreBUT
) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf.
Version optimisée pour BUT
"""
if prefs["bul_show_temporary_forced"]:
return get_filigranne(etud_etat, prefs)
if prefs["bul_show_temporary"]:
# requete les décisions de jury
decision_sem = res.etud_has_decision(etudid)
return get_filigranne(etud_etat, prefs, decision_sem=decision_sem)
return get_filigranne(etud_etat, prefs)

View File

@ -396,3 +396,13 @@ class ValidationsSemestreCache(ScoDocCache):
prefix = "VSC" prefix = "VSC"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
class RequeteTableauAssiduiteCache(ScoDocCache):
"""
clé : "<titre_tableau>:<type_obj>:<show_pres>:<show_retard>:<show_desc>:<order_col>:<order>"
Valeur = liste de dicts
"""
prefix = "TABASSI"
timeout = 60 * 60 # Une heure

View File

@ -39,7 +39,6 @@ from app import log
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc import sco_cache, sco_etud from app.scodoc import sco_cache, sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formations
from app.scodoc.codes_cursus import ( from app.scodoc.codes_cursus import (
CMP, CMP,
ADC, ADC,

View File

@ -134,10 +134,10 @@ def table_debouche_etudids(etudids, keep_numeric=True):
"nom": etud["nom"], "nom": etud["nom"],
"prenom": etud["prenom"], "prenom": etud["prenom"],
"_nom_target": url_for( "_nom_target": url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
), ),
"_prenom_target": url_for( "_prenom_target": url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
), ),
"_nom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]), "_nom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
# 'debouche' : etud['debouche'], # 'debouche' : etud['debouche'],

View File

@ -177,12 +177,26 @@ def formsemestre_edt_dict(
TODO: spécifier intervalle de dates start et end TODO: spécifier intervalle de dates start et end
""" """
t0 = time.time() t0 = time.time()
group_ids_set = set(group_ids) if group_ids else set()
try: try:
events_scodoc, _ = load_and_convert_ics(formsemestre) events_scodoc, _ = load_and_convert_ics(formsemestre)
except ScoValueError as exc: except ScoValueError as exc:
return exc.args[0] return exc.args[0]
# Génération des événements pour le calendrier html edt_dict = translate_calendar(
events_scodoc, group_ids, show_modules_titles=show_modules_titles
)
log(
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
)
return edt_dict
def translate_calendar(
events_scodoc: list[dict],
group_ids: list[int] = None,
show_modules_titles=True,
) -> list[dict]:
"""Génération des événements pour le calendrier html"""
group_ids_set = set(group_ids) if group_ids else set()
promo_icon = f"""<img height="18px" src="{scu.STATIC_DIR}/icons/promo.svg" promo_icon = f"""<img height="18px" src="{scu.STATIC_DIR}/icons/promo.svg"
title="promotion complète" alt="promotion"/>""" title="promotion complète" alt="promotion"/>"""
abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg" abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg"
@ -211,7 +225,6 @@ def formsemestre_edt_dict(
url_for( url_for(
"assiduites.signal_assiduites_group", "assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id, group_ids=group.id,
heure_deb=event["heure_deb"], heure_deb=event["heure_deb"],
heure_fin=event["heure_fin"], heure_fin=event["heure_fin"],
@ -271,17 +284,17 @@ def formsemestre_edt_dict(
"start": event["start"], "start": event["start"],
"end": event["end"], "end": event["end"],
"backgroundColor": event["group_bg_color"], "backgroundColor": event["group_bg_color"],
"raw": event["raw"],
# Infos brutes pour usage API éventuel # Infos brutes pour usage API éventuel
"edt_ens_ids": event["edt_ens_ids"], "edt_ens_ids": event["edt_ens_ids"],
"ens_user_names": ens_user_names, "ens_user_names": ens_user_names,
"group_id": group.id if group else None, "group_id": group.id if group else None,
"group_edt_id": event["edt_group"], "group_edt_id": event["edt_group"],
"moduleimpl_id": modimpl.id if modimpl else None, "moduleimpl_id": modimpl.id if modimpl else None,
"UID": event["UID"], # icalendar event UID
} }
events_cal.append(d) events_cal.append(d)
log(
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
)
return events_cal return events_cal
@ -302,7 +315,35 @@ def get_ics_uid_pattern() -> re.Pattern:
def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]: def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
"""Chargement fichier ics, filtrage et extraction des identifiants. """Chargement fichier ics.
Renvoie une liste d'évènements, et la liste des identifiants de groupes
trouvés (utilisée pour l'aide).
"""
# Chargement du calendier ics
_, calendar = formsemestre_load_calendar(formsemestre)
if not calendar:
return [], []
# --- Correspondances id edt -> id scodoc pour groupes
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
default_group = formsemestre.get_default_group()
# --- Correspondances id edt -> id scodoc pour modimpls
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
return convert_ics(
calendar,
edt2group=edt2group,
default_group=default_group,
edt2modimpl=edt2modimpl,
)
def convert_ics(
calendar: icalendar.cal.Calendar,
edt2group: dict[str, GroupDescr] = None,
default_group: GroupDescr = None,
edt2modimpl: dict[str, ModuleImpl] = None,
) -> tuple[list[dict], list[str]]:
"""Filtrage et extraction des identifiants des évènements calendrier.
Renvoie une liste d'évènements, et la liste des identifiants de groupes Renvoie une liste d'évènements, et la liste des identifiants de groupes
trouvés (utilisée pour l'aide). trouvés (utilisée pour l'aide).
@ -310,10 +351,6 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
- False si extraction regexp non configuré - False si extraction regexp non configuré
- "tous" (promo) si pas de correspondance trouvée. - "tous" (promo) si pas de correspondance trouvée.
""" """
# Chargement du calendier ics
_, calendar = formsemestre_load_calendar(formsemestre)
if not calendar:
return []
# --- Paramètres d'extraction # --- Paramètres d'extraction
edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field") edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp") edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp")
@ -348,15 +385,13 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field") edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
edt_ics_uid_pattern = get_ics_uid_pattern() edt_ics_uid_pattern = get_ics_uid_pattern()
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants # --- Groupes
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
group_colors = { group_colors = {
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1] group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
for i, group_name in enumerate(edt2group) for i, group_name in enumerate(edt2group)
} }
edt_groups_ids = set() # les ids de groupes normalisés tels que dans l'ics edt_groups_ids = set() # les ids de groupes normalisés tels que dans l'ics
default_group = formsemestre.get_default_group()
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
edt2user: dict[str, User | None] = {} # construit au fur et à mesure (cache) edt2user: dict[str, User | None] = {} # construit au fur et à mesure (cache)
# --- # ---
events = [e for e in calendar.walk() if e.name == "VEVENT"] events = [e for e in calendar.walk() if e.name == "VEVENT"]
@ -371,29 +406,6 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
) )
# title remplacé par le nom du module scodoc quand il est trouvé # title remplacé par le nom du module scodoc quand il est trouvé
title = title_edt title = title_edt
# --- Group
if edt_ics_group_pattern:
edt_group = extract_event_edt_id(
event, edt_ics_group_field, edt_ics_group_pattern
)
edt_groups_ids.add(edt_group)
# si pas de groupe dans l'event, ou si groupe non reconnu,
# prend toute la promo ("tous")
group: GroupDescr = (
edt2group.get(edt_group, default_group)
if edt_group
else default_group
)
group_bg_color = (
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
if group
else "lightgrey"
)
else:
edt_group = ""
group = False
group_bg_color = _EVENT_DEFAULT_COLOR
# --- ModuleImpl # --- ModuleImpl
if edt_ics_mod_pattern: if edt_ics_mod_pattern:
edt_module = extract_event_edt_id( edt_module = extract_event_edt_id(
@ -405,6 +417,34 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
else: else:
modimpl = False modimpl = False
edt_module = "" edt_module = ""
# --- Group
if edt_ics_group_pattern:
edt_group = extract_event_edt_id(
event, edt_ics_group_field, edt_ics_group_pattern
)
edt_groups_ids.add(edt_group)
# si pas de groupe dans l'event, ou si groupe non reconnu,
# prend toute la promo ("tous")
event_default_group = (
default_group
if default_group
else (modimpl.formsemestre.get_default_group() if modimpl else None)
)
group: GroupDescr = (
edt2group.get(edt_group, event_default_group)
if edt_group
else event_default_group
)
group_bg_color = (
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
if group
else "lightgrey"
)
else:
edt_group = ""
group = False
group_bg_color = _EVENT_DEFAULT_COLOR
# --- Enseignants # --- Enseignants
users: list[User] = [] users: list[User] = []
if edt_ics_uid_pattern: if edt_ics_uid_pattern:
@ -446,6 +486,8 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
"jour": event.decoded("dtstart").date().isoformat(), "jour": event.decoded("dtstart").date().isoformat(),
"start": event.decoded("dtstart").isoformat(), "start": event.decoded("dtstart").isoformat(),
"end": event.decoded("dtend").isoformat(), "end": event.decoded("dtend").isoformat(),
"UID": event.decoded("UID").decode("utf-8"),
"raw": event.to_ical().decode("utf-8"),
} }
) )
return events_sco, sorted(edt_groups_ids) return events_sco, sorted(edt_groups_ids)

View File

@ -542,7 +542,9 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"):
etuds = [sco_etud.get_etud_info(code_nip=nip, filled=True)[0] for nip in nips] etuds = [sco_etud.get_etud_info(code_nip=nip, filled=True)[0] for nip in nips]
for e in etuds: for e in etuds:
tgt = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]) tgt = url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
)
e["_nom_target"] = tgt e["_nom_target"] = tgt
e["_prenom_target"] = tgt e["_prenom_target"] = tgt
e["_nom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """ e["_nom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
@ -769,10 +771,10 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
e["in_scodoc"] = e["nip"] not in nips_no_sco e["in_scodoc"] = e["nip"] not in nips_no_sco
e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]] e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]]
if e["in_scodoc"]: if e["in_scodoc"]:
e["_in_scodoc_str_target"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, code_nip=e["nip"]
)
e.update(sco_etud.get_etud_info(code_nip=e["nip"], filled=True)[0]) e.update(sco_etud.get_etud_info(code_nip=e["nip"], filled=True)[0])
e["_in_scodoc_str_target"] = url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
)
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],) e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],) e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
else: else:

View File

@ -692,7 +692,7 @@ class EtapeBilan:
@staticmethod @staticmethod
def link_etu(etudid, nom): def link_etu(etudid, nom):
return '<a class="stdlink" href="%s">%s</a>' % ( return '<a class="stdlink" href="%s">%s</a>' % (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
nom, nom,
) )

View File

@ -64,7 +64,7 @@ def format_etud_ident(etud: dict):
Note: par rapport à Identite.to_dict_bul(), Note: par rapport à Identite.to_dict_bul(),
ajoute les champs: ajoute les champs:
'email_default', 'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str' 'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str'
""" """
etud["nom"] = format_nom(etud["nom"]) etud["nom"] = format_nom(etud["nom"])
if "nom_usuel" in etud: if "nom_usuel" in etud:
@ -98,10 +98,6 @@ def format_etud_ident(etud: dict):
etud["ne"] = "e" etud["ne"] = "e"
else: # 'X' else: # 'X'
etud["ne"] = "(e)" etud["ne"] = "(e)"
# Mail à utiliser pour les envois vers l'étudiant:
# choix qui pourrait être controé par une preference
# ici priorité au mail institutionnel:
etud["email_default"] = etud.get("email", "") or etud.get("emailperso", "")
def force_uppercase(s): def force_uppercase(s):
@ -117,36 +113,6 @@ def _format_etat_civil(etud: dict) -> str:
return etud["nomprenom"] return etud["nomprenom"]
def format_lycee(nomlycee):
nomlycee = nomlycee.strip()
s = nomlycee.lower()
if s[:5] == "lycee" or s[:5] == "lycée":
return nomlycee[5:]
else:
return nomlycee
def format_telephone(n):
if n is None:
return ""
if len(n) < 7:
return n
else:
n = n.replace(" ", "").replace(".", "")
i = 0
r = ""
j = len(n) - 1
while j >= 0:
r = n[j] + r
if i % 2 == 1 and j != 0:
r = " " + r
i += 1
j -= 1
if len(r) == 13 and r[0] != "0":
r = "0" + r
return r
def format_pays(s): def format_pays(s):
"laisse le pays seulement si != FRANCE" "laisse le pays seulement si != FRANCE"
if s.upper() != "FRANCE": if s.upper() != "FRANCE":
@ -283,14 +249,14 @@ def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True)
listh.append( listh.append(
f"""Autre étudiant: <a href="{ f"""Autre étudiant: <a href="{
url_for( url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=e["etudid"] etudid=e["etudid"]
)}">{e['nom']} {e['prenom']}</a>""" )}">{e['nom']} {e['prenom']}</a>"""
) )
if etudid: if etudid:
OK = "retour à la fiche étudiant" OK = "retour à la fiche étudiant"
dest_endpoint = "scolar.ficheEtud" dest_endpoint = "scolar.fiche_etud"
parameters = {"etudid": etudid} parameters = {"etudid": etudid}
else: else:
if "tf_submitted" in args: if "tf_submitted" in args:
@ -619,7 +585,7 @@ def create_etud(cnx, args: dict = None):
etud_dict = etudident_list(cnx, {"etudid": etudid})[0] etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
fill_etuds_info([etud_dict]) fill_etuds_info([etud_dict])
etud_dict["url"] = url_for( etud_dict["url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
) )
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
@ -724,19 +690,28 @@ def get_etablissements():
def get_lycee_infos(codelycee): def get_lycee_infos(codelycee):
E = get_etablissements() etablissements = get_etablissements()
return E.get(codelycee, None) return etablissements.get(codelycee, None)
def format_lycee_from_code(codelycee): def format_lycee_from_code(codelycee: str) -> str:
"Description lycee à partir du code" "Description lycee à partir du code"
E = get_etablissements() etablissements = get_etablissements()
if codelycee in E: if codelycee in etablissements:
e = E[codelycee] e = etablissements[codelycee]
nomlycee = e["name"] nomlycee = e["name"]
return "%s (%s)" % (nomlycee, e["commune"]) return f"{nomlycee} ({e['commune']})"
return f"{codelycee} (établissement inconnu)"
def format_lycee(nomlycee: str) -> str:
"mise en forme nom de lycée"
nomlycee = nomlycee.strip()
s = nomlycee.lower()
if s[:5] == "lycee" or s[:5] == "lycée":
return nomlycee[5:]
else: else:
return "%s (établissement inconnu)" % codelycee return nomlycee
def etud_add_lycee_infos(etud): def etud_add_lycee_infos(etud):
@ -821,36 +796,6 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
# nettoyage champs souvent vides # nettoyage champs souvent vides
etud["codepostallycee"] = etud.get("codepostallycee", "") or "" etud["codepostallycee"] = etud.get("codepostallycee", "") or ""
etud["nomlycee"] = etud.get("nomlycee", "") or "" etud["nomlycee"] = etud.get("nomlycee", "") or ""
if etud.get("nomlycee"):
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
if etud["villelycee"]:
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
etud["ilycee"] += "<br>"
else:
if etud.get("codelycee"):
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
else:
etud["ilycee"] = ""
rap = ""
if etud.get("rapporteur") or etud.get("commentaire"):
rap = "Note du rapporteur"
if etud.get("rapporteur"):
rap += " (%s)" % etud["rapporteur"]
rap += ": "
if etud.get("commentaire"):
rap += "<em>%s</em>" % etud["commentaire"]
etud["rap"] = rap
if etud.get("telephone"):
etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
else:
etud["telephonestr"] = ""
if etud.get("telephonemobile"):
etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
etud["telephonemobile"]
)
else:
etud["telephonemobilestr"] = ""
def etud_inscriptions_infos(etudid: int, ne="") -> dict: def etud_inscriptions_infos(etudid: int, ne="") -> dict:

View File

@ -156,7 +156,7 @@ def evaluation_check_absences_html(
H.append( H.append(
f"""<li><a class="discretelink" href="{ f"""<li><a class="discretelink" href="{
url_for( url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
) )
}">{etud.nomprenom}</a>""" }">{etud.nomprenom}</a>"""
) )

View File

@ -178,9 +178,7 @@ def evaluation_create_form(
{ {
"title": "Heure de début", "title": "Heure de début",
"explanation": "heure du début de l'épreuve", "explanation": "heure du début de l'épreuve",
"input_type": "menu", "input_type": "time",
"allowed_values": heures,
"labels": heures,
}, },
), ),
( (
@ -188,9 +186,7 @@ def evaluation_create_form(
{ {
"title": "Heure de fin", "title": "Heure de fin",
"explanation": "heure de fin de l'épreuve", "explanation": "heure de fin de l'épreuve",
"input_type": "menu", "input_type": "time",
"allowed_values": heures,
"labels": heures,
}, },
), ),
] ]
@ -335,6 +331,7 @@ def evaluation_create_form(
+ "\n" + "\n"
+ tf[1] + tf[1]
+ render_template("scodoc/help/evaluations.j2", is_apc=is_apc) + render_template("scodoc/help/evaluations.j2", is_apc=is_apc)
+ render_template("sco_timepicker.j2")
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
elif tf[0] == -1: elif tf[0] == -1:

View File

@ -691,7 +691,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
group_ids=group_id, group_ids=group_id,
evaluation_id=evaluation.id, evaluation_id=evaluation.id,
date_debut=evaluation.date_debut.isoformat(), date_debut=evaluation.date_debut.isoformat(),
date_fin=evaluation.date_fin.isoformat(), date_fin=evaluation.date_fin.isoformat() if evaluation.date_fin else "",
) )
}">absences ce jour</a> }">absences ce jour</a>
</span> </span>

View File

@ -173,9 +173,10 @@ def _build_results_list(dpv_by_sem, etuds_infos):
"nom_usuel": etud["nom_usuel"], "nom_usuel": etud["nom_usuel"],
"prenom": etud["prenom"], "prenom": etud["prenom"],
"civilite_str": etud["civilite_str"], "civilite_str": etud["civilite_str"],
"_nom_target": "%s" "_nom_target": url_for(
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
"_nom_td_attrs": 'id="%s" class="etudinfo"' % etudid, ),
"_nom_td_attrs": f'id="{etudid}" class="etudinfo"',
"bac": bac.abbrev(), "bac": bac.abbrev(),
"parcours": dec["parcours"], "parcours": dec["parcours"],
} }

View File

@ -145,7 +145,7 @@ def search_etud_in_dept(expnom=""):
if "dest_url" in vals: if "dest_url" in vals:
endpoint = vals["dest_url"] endpoint = vals["dest_url"]
else: else:
endpoint = "scolar.ficheEtud" endpoint = "scolar.fiche_etud"
if "parameters_keys" in vals: if "parameters_keys" in vals:
for key in vals["parameters_keys"].split(","): for key in vals["parameters_keys"].split(","):
url_args[key] = vals[key] url_args[key] = vals[key]
@ -328,8 +328,9 @@ def table_etud_in_accessible_depts(expnom=None):
""" """
result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom) result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
H = [ H = [
"""<div class="table_etud_in_accessible_depts">""", f"""<div class="table_etud_in_accessible_depts">
"""<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom, <h3>Recherche multi-département de "<tt>{expnom}</tt>"</h3>
""",
] ]
for etuds in result: for etuds in result:
if etuds: if etuds:
@ -337,9 +338,9 @@ def table_etud_in_accessible_depts(expnom=None):
# H.append('<h3>Département %s</h3>' % DeptId) # H.append('<h3>Département %s</h3>' % DeptId)
for e in etuds: for e in etuds:
e["_nomprenom_target"] = url_for( e["_nomprenom_target"] = url_for(
"scolar.ficheEtud", scodoc_dept=dept_id, etudid=e["etudid"] "scolar.fiche_etud", scodoc_dept=dept_id, etudid=e["etudid"]
) )
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"]) e["_nomprenom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
tab = GenTable( tab = GenTable(
titles={"nomprenom": "Étudiants en " + dept_id}, titles={"nomprenom": "Étudiants en " + dept_id},

View File

@ -102,7 +102,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
scodoc_dept=g.scodoc_dept, etudid=etudid, only_ext=1) }"> scodoc_dept=g.scodoc_dept, etudid=etudid, only_ext=1) }">
inscrire à un autre semestre</a>" inscrire à un autre semestre</a>"
</p> </p>
<h3><a href="{ url_for('scolar.ficheEtud', <h3><a href="{ url_for('scolar.fiche_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid)
}" class="stdlink">Étudiant {etud.nomprenom}</a></h3> }" class="stdlink">Étudiant {etud.nomprenom}</a></h3>
""", """,
@ -221,7 +221,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
tf[2]["formation_id"] = orig_sem["formation_id"] tf[2]["formation_id"] = orig_sem["formation_id"]
formsemestre_ext_create(etudid, tf[2]) formsemestre_ext_create(etudid, tf[2])
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )

View File

@ -400,7 +400,7 @@ def formsemestre_inscription_with_modules_form(etudid, only_ext=False):
H.append("<p>aucune session de formation !</p>") H.append("<p>aucune session de formation !</p>")
H.append( H.append(
f"""<h3>ou</h3> <a class="stdlink" href="{ f"""<h3>ou</h3> <a class="stdlink" href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">retour à la fiche de {etud.nomprenom}</a>""" }">retour à la fiche de {etud.nomprenom}</a>"""
) )
return "\n".join(H) + footer return "\n".join(H) + footer
@ -440,7 +440,7 @@ def formsemestre_inscription_with_modules(
dans le semestre {formsemestre.titre_mois()} dans le semestre {formsemestre.titre_mois()}
</p> </p>
<ul> <ul>
<li><a href="{url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) <li><a href="{url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}" class="stdlink">retour à la fiche de {etud.nomprenom}</a> }" class="stdlink">retour à la fiche de {etud.nomprenom}</a>
</li> </li>
<li><a href="{url_for( <li><a href="{url_for(
@ -501,7 +501,7 @@ def formsemestre_inscription_with_modules(
method="formsemestre_inscription_with_modules", method="formsemestre_inscription_with_modules",
) )
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
else: else:
# formulaire choix groupe # formulaire choix groupe
@ -656,7 +656,7 @@ function chkbx_select(field_id, state) {
return "\n".join(H) + "\n" + tf[1] + footer return "\n".join(H) + "\n" + tf[1] + footer
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
else: else:
# Inscriptions aux modules choisis # Inscriptions aux modules choisis
@ -697,7 +697,7 @@ function chkbx_select(field_id, state) {
"""<h3>Aucune modification à effectuer</h3> """<h3>Aucune modification à effectuer</h3>
<p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p> <p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p>
""" """
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) % url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
return "\n".join(H) + footer return "\n".join(H) + footer
@ -755,7 +755,7 @@ function chkbx_select(field_id, state) {
etudid, etudid,
modulesimpls_ainscrire, modulesimpls_ainscrire,
modulesimpls_adesinscrire, modulesimpls_adesinscrire,
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
) )
) )
return "\n".join(H) + footer return "\n".join(H) + footer
@ -820,7 +820,7 @@ def do_moduleimpl_incription_options(
<p><a class="stdlink" href="%s"> <p><a class="stdlink" href="%s">
Retour à la fiche étudiant</a></p> Retour à la fiche étudiant</a></p>
""" """
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), % url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
return "\n".join(H) return "\n".join(H)
@ -885,7 +885,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
'<li><a href="%s" class="discretelink">%s</a> : ' '<li><a href="%s" class="discretelink">%s</a> : '
% ( % (
url_for( url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud["etudid"],
), ),

View File

@ -51,13 +51,14 @@ from app.models import (
NotesNotes, NotesNotes,
) )
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
ScoValueError, ScoValueError,
ScoInvalidIdType, ScoInvalidIdType,
) )
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_archives_formsemestre from app.scodoc import sco_archives_formsemestre
@ -75,6 +76,7 @@ from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
import sco_version import sco_version
@ -109,7 +111,7 @@ def _build_menu_stats(formsemestre_id):
"title": "Lycées d'origine", "title": "Lycées d'origine",
"endpoint": "notes.formsemestre_etuds_lycees", "endpoint": "notes.formsemestre_etuds_lycees",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": True, "enabled": current_user.has_permission(Permission.ViewEtudData),
}, },
{ {
"title": 'Table "poursuite"', "title": 'Table "poursuite"',
@ -336,6 +338,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
formsemestre_id, fix_if_missing=True formsemestre_id, fix_if_missing=True
), ),
}, },
"enabled": current_user.has_permission(Permission.ViewEtudData),
}, },
{ {
"title": "Vérifier inscriptions multiples", "title": "Vérifier inscriptions multiples",
@ -474,57 +477,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
return "\n".join(H) return "\n".join(H)
def retreive_formsemestre_from_request() -> int:
"""Cherche si on a de quoi déduire le semestre affiché à partir des
arguments de la requête:
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
Returns None si pas défini.
"""
if request.method == "GET":
args = request.args
elif request.method == "POST":
args = request.form
else:
return None
formsemestre_id = None
# Search formsemestre
group_ids = args.get("group_ids", [])
if "formsemestre_id" in args:
formsemestre_id = args["formsemestre_id"]
elif "moduleimpl_id" in args and args["moduleimpl_id"]:
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"])
if not modimpl:
return None # suppressed ?
modimpl = modimpl[0]
formsemestre_id = modimpl["formsemestre_id"]
elif "evaluation_id" in args:
E = sco_evaluation_db.get_evaluations_dict(
{"evaluation_id": args["evaluation_id"]}
)
if not E:
return None # evaluation suppressed ?
E = E[0]
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = modimpl["formsemestre_id"]
elif "group_id" in args:
group = sco_groups.get_group(args["group_id"])
formsemestre_id = group["formsemestre_id"]
elif group_ids:
if isinstance(group_ids, str):
group_ids = group_ids.split(",")
group_id = group_ids[0]
group = sco_groups.get_group(group_id)
formsemestre_id = group["formsemestre_id"]
elif "partition_id" in args:
partition = sco_groups.get_partition(args["partition_id"])
formsemestre_id = partition["formsemestre_id"]
if not formsemestre_id:
return None # no current formsemestre
return int(formsemestre_id)
# Element HTML decrivant un semestre (barre de menu et infos) # Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title(formsemestre_id=None): def formsemestre_page_title(formsemestre_id=None):
"""Element HTML decrivant un semestre (barre de menu et infos) """Element HTML decrivant un semestre (barre de menu et infos)
@ -917,7 +869,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
<a class="btn" href="{ <a class="btn" href="{
url_for("assiduites.bilan_dept", url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre=formsemestre.id, formsemestre_id=formsemestre.id,
group_ids=group.id, group_ids=group.id,
)}"> )}">
<button>Justificatifs en attente</button></a> <button>Justificatifs en attente</button></a>
@ -1457,7 +1409,7 @@ def formsemestre_warning_etuds_sans_note(
noms = ", ".join( noms = ", ".join(
[ [
f"""<a href="{ f"""<a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}" class="discretelink">{etud.nomprenom}</a>""" }" class="discretelink">{etud.nomprenom}</a>"""
for etud in etuds for etud in etuds
] ]
@ -1519,13 +1471,13 @@ def formsemestre_note_etuds_sans_notes(
a déjà des notes""" a déjà des notes"""
) )
return redirect( return redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) )
else: else:
noms = "</li><li>".join( noms = "</li><li>".join(
[ [
f"""<a href="{ f"""<a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}" class="discretelink">{etud.nomprenom}</a>""" }" class="discretelink">{etud.nomprenom}</a>"""
for etud in etuds for etud in etuds
] ]

View File

@ -117,8 +117,8 @@ def formsemestre_validation_etud_form(
if read_only: if read_only:
check = True check = True
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) Se = sco_cursus.get_situation_etud_cursus(etud_d, formsemestre_id)
if not Se.sem["etat"]: if not Se.sem["etat"]:
raise ScoValueError("validation: semestre verrouille") raise ScoValueError("validation: semestre verrouille")
@ -132,7 +132,7 @@ def formsemestre_validation_etud_form(
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=f"Parcours {etud['nomprenom']}", page_title=f"Parcours {etud.nomprenom}",
javascripts=["js/recap_parcours.js"], javascripts=["js/recap_parcours.js"],
) )
] ]
@ -177,26 +177,22 @@ def formsemestre_validation_etud_form(
H.append('<table style="width: 100%"><tr><td>') H.append('<table style="width: 100%"><tr><td>')
if not check: if not check:
H.append( H.append(
'<h2 class="formsemestre">%s: validation %s%s</h2>Parcours: %s' f"""<h2 class="formsemestre">{etud.nomprenom}: validation {
% ( Se.parcours.SESSION_NAME_A}{Se.parcours.SESSION_NAME
etud["nomprenom"], }</h2>Parcours: {Se.get_cursus_descr()}
Se.parcours.SESSION_NAME_A, """
Se.parcours.SESSION_NAME,
Se.get_cursus_descr(),
)
) )
else: else:
H.append( H.append(
'<h2 class="formsemestre">Parcours de %s</h2>%s' f"""<h2 class="formsemestre">Parcours de {etud.nomprenom}</h2>{Se.get_cursus_descr()}"""
% (etud["nomprenom"], Se.get_cursus_descr())
) )
H.append( H.append(
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>' f"""</td><td style="text-align: right;"><a href="{
% ( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a></td></tr>
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]), </table>
) """
) )
etud_etat = nt.get_etud_etat(etudid) etud_etat = nt.get_etud_etat(etudid)
@ -210,7 +206,7 @@ def formsemestre_validation_etud_form(
<div class="warning"> <div class="warning">
Impossible de statuer sur cet étudiant: Impossible de statuer sur cet étudiant:
il est démissionnaire ou défaillant (voir <a href="{ il est démissionnaire ou défaillant (voir <a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">sa fiche</a>) }">sa fiche</a>)
</div> </div>
""" """
@ -289,7 +285,7 @@ def formsemestre_validation_etud_form(
etudid=etudid, origin_formsemestre_id=formsemestre_id etudid=etudid, origin_formsemestre_id=formsemestre_id
).all() ).all()
if autorisations: if autorisations:
H.append(". Autorisé%s à s'inscrire en " % etud["ne"]) H.append(f". Autorisé{etud.e} à s'inscrire en ")
H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".") H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".")
H.append("</p>") H.append("</p>")

View File

@ -52,7 +52,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_etud import etud_sort_key from app.scodoc.sco_etud import etud_sort_key
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
@ -118,6 +118,16 @@ def groups_view(
init_qtip=True, init_qtip=True,
) )
} }
<style>
div.multiselect-container.dropdown-menu {{
min-width: 180px;
}}
span.warning_unauthorized {{
color: pink;
font-style: italic;
margin-left: 12px;
}}
</style>
<div id="group-tabs"> <div id="group-tabs">
<!-- Menu choix groupe --> <!-- Menu choix groupe -->
{form_groups_choice(groups_infos, submit_on_change=True)} {form_groups_choice(groups_infos, submit_on_change=True)}
@ -474,15 +484,12 @@ def groups_table(
""" """
from app.scodoc import sco_report from app.scodoc import sco_report
# log( can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
# "enter groups_table %s: %s"
# % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-"))
# )
with_codes = int(with_codes) with_codes = int(with_codes)
with_paiement = int(with_paiement) with_paiement = int(with_paiement) and can_view_etud_data
with_archives = int(with_archives) with_archives = int(with_archives) and can_view_etud_data
with_annotations = int(with_annotations) with_annotations = int(with_annotations) and can_view_etud_data
with_bourse = int(with_bourse) with_bourse = int(with_bourse) and can_view_etud_data
base_url_np = groups_infos.base_url + f"&with_codes={with_codes}" base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
base_url = ( base_url = (
@ -527,7 +534,8 @@ def groups_table(
if fmt != "html": # ne mentionne l'état que en Excel (style en html) if fmt != "html": # ne mentionne l'état que en Excel (style en html)
columns_ids.append("etat") columns_ids.append("etat")
columns_ids.append("email") columns_ids.append("email")
columns_ids.append("emailperso") if can_view_etud_data:
columns_ids.append("emailperso")
if fmt == "moodlecsv": if fmt == "moodlecsv":
columns_ids = ["email", "semestre_groupe"] columns_ids = ["email", "semestre_groupe"]
@ -561,7 +569,7 @@ def groups_table(
else: else:
etud["_emailperso_target"] = "" etud["_emailperso_target"] = ""
fiche_url = url_for( fiche_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
) )
etud["_nom_disp_target"] = fiche_url etud["_nom_disp_target"] = fiche_url
etud["_nom_disp_order"] = etud_sort_key(etud) etud["_nom_disp_order"] = etud_sort_key(etud)
@ -616,7 +624,7 @@ def groups_table(
+ "+".join(sorted(moodle_groupenames)) + "+".join(sorted(moodle_groupenames))
) )
else: else:
filename = "etudiants_%s" % groups_infos.groups_filename filename = f"etudiants_{groups_infos.groups_filename}"
prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id)
tab = GenTable( tab = GenTable(
@ -664,28 +672,33 @@ def groups_table(
""" """
] ]
if groups_infos.members: if groups_infos.members:
Of = [] menu_options = []
options = { options = {
"with_paiement": "Paiement inscription", "with_codes": "Affiche codes",
"with_archives": "Fichiers archivés",
"with_annotations": "Annotations",
"with_codes": "Codes",
"with_bourse": "Statut boursier",
} }
for option in options: if can_view_etud_data:
options.update(
{
"with_paiement": "Paiement inscription",
"with_archives": "Fichiers archivés",
"with_annotations": "Annotations",
"with_bourse": "Statut boursier",
}
)
for option, label in options.items():
if locals().get(option, False): if locals().get(option, False):
selected = "selected" selected = "selected"
else: else:
selected = "" selected = ""
Of.append( menu_options.append(
"""<option value="%s" %s>%s</option>""" f"""<option value="{option}" {selected}>{label}</option>"""
% (option, selected, options[option])
) )
H.extend( H.extend(
[ [
"""<span style="margin-left: 2em;"><select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""", """<span style="margin-left: 2em;">
"\n".join(Of), <select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
"\n".join(menu_options),
"""</select></span> """</select></span>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
@ -701,6 +714,9 @@ def groups_table(
}); });
</script> </script>
""", """,
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
if not can_view_etud_data
else "",
] ]
) )
H.append("</div></form>") H.append("</div></form>")
@ -708,41 +724,45 @@ def groups_table(
H.extend( H.extend(
[ [
tab.html(), tab.html(),
"<ul>", f"""
'<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d\'appel Excel</a></li>' <ul>
% (tab.base_url,), <li><a class="stdlink" href="{tab.base_url}&fmt=xlsappel">Feuille d'appel Excel</a>
'<li><a class="stdlink" href="%s&fmt=xls">Table Excel</a></li>' </li>
% (tab.base_url,), <li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
'<li><a class="stdlink" href="%s&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>' </li>
% (tab.base_url,), <li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a>
"""<li> </li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a> <li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id={groups_infos.formsemestre_id}">
Fichier CSV pour Moodle (tous les groupes)</a>
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em> <em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
</li>""" </li>""",
% groups_infos.formsemestre_id,
] ]
) )
if amail_inst: if amail_inst:
H.append( H.append(
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses institutionnelles)</a></li>' f"""<li>
% ( <a class="stdlink" href="mailto:?bcc={','.join(amail_inst)
",".join(amail_inst), }">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
groups_infos.groups_titles, (via {len(amail_inst)} adresses institutionnelles)</a>
len(amail_inst), </li>"""
)
) )
if amail_perso: if can_view_etud_data:
H.append( if amail_perso:
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses personnelles)</a></li>' H.append(
% ( f"""<li>
",".join(amail_perso), <a class="stdlink" href="mailto:?bcc={','.join(amail_perso)
groups_infos.groups_titles, }">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
len(amail_perso), (via {len(amail_perso)} adresses personnelles)</a>
</li>"""
) )
) else:
H.append("<li><em>Adresses personnelles non renseignées</em></li>")
else: else:
H.append("<li><em>Adresses personnelles non renseignées</em></li>") H.append(
"""<li class="unauthorized">adresses mail personnelles protégées</li>"""
)
H.append("</ul>") H.append("</ul>")
@ -772,6 +792,10 @@ def groups_table(
filename = "liste_%s" % groups_infos.groups_filename filename = "liste_%s" % groups_infos.groups_filename
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
elif fmt == "allxls": elif fmt == "allxls":
if not can_view_etud_data:
raise ScoPermissionDenied(
"Vous n'avez pas la permission requise (ViewEtudData)"
)
# feuille Excel avec toutes les infos etudiants # feuille Excel avec toutes les infos etudiants
if not groups_infos.members: if not groups_infos.members:
return "" return ""
@ -829,7 +853,9 @@ def groups_table(
etud, groups_infos.formsemestre_id etud, groups_infos.formsemestre_id
) )
m["parcours"] = Se.get_cursus_descr() m["parcours"] = Se.get_cursus_descr()
m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud) m["code_cursus"], _ = sco_report.get_code_cursus_etud(
etud["etudid"], sems=etud["sems"]
)
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members] rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
title = "etudiants_%s" % groups_infos.groups_filename title = "etudiants_%s" % groups_infos.groups_filename
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title) xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
@ -879,8 +905,9 @@ def tab_absences_html(groups_infos, etat=None):
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>""" """<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>""" f"""<li><a class="stdlink" href="groups_export_annotations?{groups_infos.groups_query_args}">Liste des annotations</a></li>"""
% groups_infos.groups_query_args, if authuser.has_permission(Permission.ViewEtudData)
else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>""",
"</ul>", "</ul>",
] ]
) )
@ -899,14 +926,19 @@ def tab_absences_html(groups_infos, etat=None):
""" """
) )
# Lien pour ajout fichiers étudiants # Lien pour ajout fichiers étudiants
if authuser.has_permission(Permission.EtudAddAnnotations): text = "Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)"
if authuser.has_permission(
Permission.EtudAddAnnotations
) and authuser.has_permission(Permission.ViewEtudData):
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for('scolar.etudarchive_import_files_form', url_for('scolar.etudarchive_import_files_form',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
group_id=group_id group_id=group_id
)}">Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)</a></li>""" )}">{text}</a></li>"""
) )
else:
H.append(f"""<li class="unauthorized" title="non autorisé">{text}</li>""")
H.append("</ul></div>") H.append("</ul></div>")
return "".join(H) return "".join(H)

View File

@ -669,7 +669,7 @@ def etuds_select_boxes(
elink = """<a class="discretelink %s" href="%s">%s</a>""" % ( elink = """<a class="discretelink %s" href="%s">%s</a>""" % (
c, c,
url_for( url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud["etudid"],
), ),

View File

@ -29,6 +29,7 @@
""" """
from collections import defaultdict from collections import defaultdict
import numpy as np import numpy as np
import pandas as pd
import flask import flask
from flask import url_for, g, request from flask import url_for, g, request
@ -264,7 +265,7 @@ def _make_table_notes(
if e.moduleimpl_id != modimpl.id: if e.moduleimpl_id != modimpl.id:
raise ValueError("invalid evaluations list") raise ValueError("invalid evaluations list")
if fmt == "xls": if fmt == "xls" or fmt == "json":
keep_numeric = True # pas de conversion des notes en strings keep_numeric = True # pas de conversion des notes en strings
else: else:
keep_numeric = False keep_numeric = False
@ -279,11 +280,12 @@ def _make_table_notes(
if anonymous_listing: if anonymous_listing:
columns_ids = ["code"] # cols in table columns_ids = ["code"] # cols in table
else: else:
if fmt == "xls" or fmt == "xml": if fmt in {"xls", "xml", "json"}:
columns_ids = ["nom", "prenom"] columns_ids = ["etudid", "nom", "prenom"]
else: else:
columns_ids = ["nomprenom"] columns_ids = ["nomprenom"]
if not hide_groups: if not hide_groups and fmt not in {"xls", "xml", "json"}:
# n'indique pas les groupes en xls, json car notation "humaine" ici
columns_ids.append("group") columns_ids.append("group")
titles = { titles = {
@ -476,7 +478,7 @@ def _make_table_notes(
if with_emails: if with_emails:
columns_ids += ["email", "emailperso"] columns_ids += ["email", "emailperso"]
# Ajoute lignes en tête et moyennes # Ajoute lignes en tête et moyennes
if len(evaluations) > 0 and fmt != "bordereau": if len(evaluations) > 0 and fmt != "bordereau" and fmt != "json":
rows_head = [row_coefs] rows_head = [row_coefs]
if is_apc: if is_apc:
rows_head.append(row_poids) rows_head.append(row_poids)
@ -683,7 +685,7 @@ def _make_table_notes(
def _add_eval_columns( def _add_eval_columns(
evaluation: Evaluation, evaluation: Evaluation,
eval_state, eval_state,
evals_poids, evals_poids: pd.DataFrame | None,
ues, ues,
rows, rows,
titles, titles,
@ -833,14 +835,22 @@ def _add_eval_columns(
return notes, nb_abs, nb_att # pour histogramme return notes, nb_abs, nb_att # pour histogramme
def _mini_table_eval_ue_poids(evaluation_id, evals_poids, ues): def _mini_table_eval_ue_poids(
evaluation_id: int, evals_poids: pd.DataFrame, ues
) -> str:
"contenu de la cellule: poids" "contenu de la cellule: poids"
ue_poids = [
(ue.acronyme, evals_poids[ue.id][evaluation_id])
for ue in ues
if (evals_poids[ue.id][evaluation_id] or 0) > 0
]
return ( return (
"""<table class="eval_poids" title="poids vers les UE"><tr><td>""" """<table class="eval_poids" title="poids vers les UE"><tr><td>"""
+ "</td><td>".join([f"{ue.acronyme}" for ue in ues]) + "</td><td>".join([f"{up[0]}" for up in ue_poids])
+ "</td></tr>" + "</td></tr>"
+ "<tr><td>" + "<tr><td>"
+ "</td><td>".join([f"{evals_poids[ue.id][evaluation_id]}" for ue in ues]) + "</td><td>".join([f"{up[1]}" for up in ue_poids])
+ "</td></tr></table>" + "</td></tr></table>"
) )

View File

@ -143,7 +143,9 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
if not no_links: if not no_links:
for etud in etuds: for etud in etuds:
fiche_url = url_for( fiche_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
) )
etud["_nom_target"] = fiche_url etud["_nom_target"] = fiche_url
etud["_prenom_target"] = fiche_url etud["_prenom_target"] = fiche_url
@ -232,7 +234,7 @@ def js_coords_lycees(etuds_by_lycee):
'<a class="discretelink" href="%s" title="">%s</a>' '<a class="discretelink" href="%s" title="">%s</a>'
% ( % (
url_for( url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=e["etudid"], etudid=e["etudid"],
), ),

View File

@ -40,6 +40,7 @@ from app.comp.res_compat import NotesTableCompat
from app.models import ( from app.models import (
FormSemestre, FormSemestre,
Identite, Identite,
ModuleImpl,
Partition, Partition,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
UniteEns, UniteEns,
@ -52,7 +53,6 @@ from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -63,7 +63,9 @@ import app.scodoc.sco_utils as scu
from app.tables import list_etuds from app.tables import list_etuds
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False): def moduleimpl_inscriptions_edit(
moduleimpl_id, etudids: list[int] | None = None, submitted=False
):
"""Formulaire inscription des etudiants a ce module """Formulaire inscription des etudiants a ce module
* Gestion des inscriptions * Gestion des inscriptions
Nom TD TA TP (triable) Nom TD TA TP (triable)
@ -75,12 +77,12 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
* Si pas les droits: idem en readonly * Si pas les droits: idem en readonly
""" """
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] etudids = etudids or []
formsemestre_id = M["formsemestre_id"] modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] module = modimpl.module
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre = modimpl.formsemestre
# -- check lock # -- check lock
if not sem["etat"]: if not formsemestre.etat:
raise ScoValueError("opération impossible: semestre verrouille") raise ScoValueError("opération impossible: semestre verrouille")
header = html_sco_header.sco_header( header = html_sco_header.sco_header(
page_title="Inscription au module", page_title="Inscription au module",
@ -90,25 +92,23 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
H = [ H = [
header, header,
"""<h2>Inscriptions au module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a> (%s)</a></h2> f"""<h2>Inscriptions au module <a class="stdlink" href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id)
}">{module.titre or "(module sans titre)"}</a> ({module.code})</a></h2>
<p class="help">Cette page permet d'éditer les étudiants inscrits à ce module <p class="help">Cette page permet d'éditer les étudiants inscrits à ce module
(ils doivent évidemment être inscrits au semestre). (ils doivent évidemment être inscrits au semestre).
Les étudiants cochés sont (ou seront) inscrits. Vous pouvez facilement inscrire ou Les étudiants cochés sont (ou seront) inscrits. Vous pouvez inscrire ou
désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever". désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
</p> </p>
<p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas sur le bouton <p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas
"Appliquer les modifications". sur le bouton "Appliquer les modifications".
</p> </p>
""" """,
% (
moduleimpl_id,
mod["titre"] or "(module sans titre)",
mod["code"] or "(module sans code)",
),
] ]
# Liste des inscrits à ce semestre # Liste des inscrits à ce semestre
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
formsemestre_id formsemestre.id
) )
for ins in inscrits: for ins in inscrits:
etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1) etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1)
@ -121,12 +121,10 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
) )
ins["etud"] = etuds_info[0] ins["etud"] = etuds_info[0]
inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"])) inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
in_m = sco_moduleimpl.do_moduleimpl_inscription_list( in_m = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
moduleimpl_id=M["moduleimpl_id"] in_module = {x["etudid"] for x in in_m}
)
in_module = set([x["etudid"] for x in in_m])
# #
partitions = sco_groups.get_partitions_list(formsemestre_id) partitions = sco_groups.get_partitions_list(formsemestre.id)
# #
if not submitted: if not submitted:
H.append( H.append(
@ -149,27 +147,32 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
} }
} }
</script>""" </script>
<style>
table.mi_table td, table.mi_table th {
text-align: left;
}
</style>
"""
) )
H.append( H.append(
f"""<form method="post" id="mi_form" action="{request.base_url}"> f"""<form method="post" id="mi_form" action="{request.base_url}">
<input type="hidden" name="moduleimpl_id" value="{M['moduleimpl_id']}"/> <input type="hidden" name="moduleimpl_id" value="{modimpl.id}"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/> <input type="submit" name="submitted" value="Appliquer les modifications"/>
<p></p> <div>
<table><tr> { _make_menu(partitions, "Ajouter", "true") }
{ _make_menu(partitions, "Ajouter", "true") } { _make_menu(partitions, "Enlever", "false")}
{ _make_menu(partitions, "Enlever", "false")} </div>
</tr></table> <table class="gt_table mi_table">
<p><br></p> <thead>
<table class="sortable" id="mi_table">
<tr> <tr>
<th>Nom</th> <th class="etud">Nom</th>
""" """
) )
for partition in partitions: for partition in partitions:
if partition["partition_name"]: if partition["partition_name"]:
H.append("<th>%s</th>" % partition["partition_name"]) H.append(f"<th>{partition['partition_name']}</th>")
H.append("</tr>") H.append("</tr></thead><tbody>")
for ins in inscrits: for ins in inscrits:
etud = ins["etud"] etud = ins["etud"]
@ -178,24 +181,20 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
else: else:
checked = "" checked = ""
H.append( H.append(
"""<tr><td><input type="checkbox" name="etuds:list" value="%s" %s>""" f"""<tr><td class="etud"><input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>"""
% (etud["etudid"], checked)
) )
H.append( H.append(
"""<a class="discretelink etudinfo" href="%s" id="%s">%s</a>""" f"""<a class="discretelink etudinfo" href="{
% (
url_for( url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud["etudid"],
), )
etud["etudid"], }" id="{etud['etudid']}">{etud['nomprenom']}</a>"""
etud["nomprenom"],
)
) )
H.append("""</input></td>""") H.append("""</input></td>""")
groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre_id) groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id)
for partition in partitions: for partition in partitions:
if partition["partition_name"]: if partition["partition_name"]:
gr_name = "" gr_name = ""
@ -205,11 +204,11 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
break break
# gr_name == '' si etud non inscrit dans un groupe de cette partition # gr_name == '' si etud non inscrit dans un groupe de cette partition
H.append(f"<td>{gr_name}</td>") H.append(f"<td>{gr_name}</td>")
H.append("""</table></form>""") H.append("""</tbody></table></form>""")
else: # SUBMISSION else: # SUBMISSION
# inscrit a ce module tous les etuds selectionnes # inscrit a ce module tous les etuds selectionnes
sco_moduleimpl.do_moduleimpl_inscrit_etuds( sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre_id, etuds, reset=True moduleimpl_id, formsemestre.id, etudids, reset=True
) )
return flask.redirect( return flask.redirect(
url_for( url_for(
@ -225,10 +224,10 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
def _make_menu(partitions: list[dict], title="", check="true") -> str: def _make_menu(partitions: list[dict], title="", check="true") -> str:
"""Menu with list of all groups""" """Menu with list of all groups"""
items = [{"title": "Tous", "attr": "onclick=\"group_select('', -1, %s)\"" % check}] items = [{"title": "Tous", "attr": f"onclick=\"group_select('', -1, {check})\""}]
p_idx = 0 p_idx = 0
for partition in partitions: for partition in partitions:
if partition["partition_name"] != None: if partition["partition_name"] is not None:
p_idx += 1 p_idx += 1
for group in sco_groups.get_partition_groups(partition): for group in sco_groups.get_partition_groups(partition):
items.append( items.append(
@ -240,9 +239,9 @@ def _make_menu(partitions: list[dict], title="", check="true") -> str:
} }
) )
return ( return (
'<td class="inscr_addremove_menu">' '<div class="inscr_addremove_menu">'
+ htmlutils.make_menu(title, items, alone=True) + htmlutils.make_menu(title, items, alone=True)
+ "</td>" + "</div>"
) )
@ -420,9 +419,11 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
for info in ues_cap_info[ue["ue_id"]]: for info in ues_cap_info[ue["ue_id"]]:
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0] etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
H.append( H.append(
f"""<li class="etud"><a class="discretelink" href="{ f"""<li class="etud"><a class="discretelink etudinfo"
id="{info['etudid']}"
href="{
url_for( url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud["etudid"],
) )
@ -543,7 +544,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
H.append( H.append(
f"""<tr><td><a class="discretelink etudinfo" id={etud.id} f"""<tr><td><a class="discretelink etudinfo" id={etud.id}
href="{url_for( href="{url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud.id, etudid=etud.id,
)}" )}"
@ -695,7 +696,7 @@ def _fmt_etud_set(etudids, max_list_size=7) -> str:
[ [
f"""<a class="discretelink" href="{ f"""<a class="discretelink" href="{
url_for( url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
) )
}">{etud.nomprenom}</a>""" }">{etud.nomprenom}</a>"""
for etud in sorted(etuds, key=attrgetter("sort_key")) for etud in sorted(etuds, key=attrgetter("sort_key"))

View File

@ -134,7 +134,8 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
if evaluation.date_fin if evaluation.date_fin
else "", else "",
}, },
"enabled": evaluation.date_debut is not None, "enabled": evaluation.date_debut is not None
and evaluation.date_fin is not None,
}, },
{ {
"title": "Vérifier notes vs absents", "title": "Vérifier notes vs absents",
@ -167,6 +168,7 @@ def _ue_coefs_html(coefs_lst) -> str:
{'background-color: ' + ue.color + ';' if ue.color else ''} {'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>""" "><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst for ue, coef in coefs_lst
if coef > 0
] ]
) )
+ "</div>" + "</div>"

View File

@ -25,38 +25,37 @@
# #
############################################################################## ##############################################################################
"""ScoDoc ficheEtud """ScoDoc fiche_etud
Fiche description d'un étudiant et de son parcours Fiche description d'un étudiant et de son parcours
""" """
from flask import abort, url_for, g, render_template, request from flask import url_for, g, render_template, request
from flask_login import current_user from flask_login import current_user
import sqlalchemy as sa
from app import db, log from app import log
from app.auth.models import User
from app.but import cursus_but from app.but import cursus_but
from app.models.etudiants import make_etud_args from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
from app.models import Identite, FormSemestre, ScoDocSiteConfig from app.scodoc import (
from app.scodoc import html_sco_header codes_cursus,
from app.scodoc import htmlutils html_sco_header,
from app.scodoc import sco_archives_etud htmlutils,
from app.scodoc import sco_bac sco_archives_etud,
from app.scodoc import codes_cursus sco_bac,
from app.scodoc import sco_formsemestre sco_cursus,
from app.scodoc import sco_formsemestre_status sco_etud,
from app.scodoc import sco_groups sco_groups,
from app.scodoc import sco_cursus sco_permissions_check,
from app.scodoc import sco_permissions_check sco_report,
from app.scodoc import sco_photos )
from app.scodoc import sco_users from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc import sco_report
from app.scodoc import sco_etud
from app.scodoc.sco_bulletins import etud_descr_situation_semestre from app.scodoc.sco_bulletins import etud_descr_situation_semestre
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
def _menu_scolarite( def _menu_scolarite(
@ -157,29 +156,18 @@ def _menu_scolarite(
) )
def ficheEtud(etudid=None): def fiche_etud(etudid=None):
"fiche d'informations sur un etudiant" "fiche d'informations sur un etudiant"
authuser = current_user restrict_etud_data = not current_user.has_permission(Permission.ViewEtudData)
cnx = ndb.GetDBConnexion() try:
if etudid: etud = Identite.get_etud(etudid)
try: # pour les bookmarks avec d'anciens ids... except Exception as exc:
etudid = int(etudid) log(f"fiche_etud: etudid={etudid!r} request.args={request.args!r}")
except ValueError: raise ScoValueError("Étudiant inexistant !") from exc
raise ScoValueError("id invalide !") from ValueError # la sidebar est differente s'il y a ou pas un etudid
# la sidebar est differente s'il y a ou pas un etudid # voir html_sidebar.sidebar()
# voir html_sidebar.sidebar() g.etudid = etudid
g.etudid = etudid info = etud.to_dict_scodoc7(restrict=restrict_etud_data)
args = make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args)
if not etuds:
log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}")
raise ScoValueError("Étudiant inexistant !")
etud_ = etuds[0] # transition: etud_ à éliminer et remplacer par etud
etudid = etud_["etudid"]
etud = Identite.get_etud(etudid)
sco_etud.fill_etuds_info([etud_])
#
info = etud_
if etud.prenom_etat_civil: if etud.prenom_etat_civil:
info["etat_civil"] = ( info["etat_civil"] = (
"<h3>Etat-civil: " "<h3>Etat-civil: "
@ -193,45 +181,26 @@ def ficheEtud(etudid=None):
else: else:
info["etat_civil"] = "" info["etat_civil"] = ""
info["ScoURL"] = scu.ScoURL() info["ScoURL"] = scu.ScoURL()
info["authuser"] = authuser info["authuser"] = current_user
info["info_naissance"] = info["date_naissance"] if restrict_etud_data:
if info["lieu_naissance"]: info["info_naissance"] = ""
info["info_naissance"] += " à " + info["lieu_naissance"] adresse = None
if info["dept_naissance"]:
info["info_naissance"] += f" ({info['dept_naissance']})"
info["etudfoto"] = sco_photos.etud_photo_html(etud_)
if (
(not info["domicile"])
and (not info["codepostaldomicile"])
and (not info["villedomicile"])
):
info["domicile"] = "<em>inconnue</em>"
if info["paysdomicile"]:
pays = sco_etud.format_pays(info["paysdomicile"])
if pays:
info["paysdomicile"] = "(%s)" % pays
else:
info["paysdomicile"] = ""
if info["telephone"] or info["telephonemobile"]:
info["telephones"] = "<br>%s &nbsp;&nbsp; %s" % (
info["telephonestr"],
info["telephonemobilestr"],
)
else: else:
info["telephones"] = "" info["info_naissance"] = info["date_naissance"]
# e-mail: if info["lieu_naissance"]:
if info["email_default"]: info["info_naissance"] += " à " + info["lieu_naissance"]
info["emaillink"] = ", ".join( if info["dept_naissance"]:
[ info["info_naissance"] += f" ({info['dept_naissance']})"
'<a class="stdlink" href="mailto:%s">%s</a>' % (m, m) adresse = etud.adresses.first()
for m in [etud_["email"], etud_["emailperso"]] info.update(_format_adresse(adresse))
if m
] info.update(etud.inscription_descr())
) info["etudfoto"] = etud.photo_html()
else:
info["emaillink"] = "<em>(pas d'adresse e-mail)</em>"
# Champ dépendant des permissions: # Champ dépendant des permissions:
if authuser.has_permission(Permission.EtudChangeAdr): if current_user.has_permission(
Permission.EtudChangeAdr
) and current_user.has_permission(Permission.ViewEtudData):
info[ info[
"modifadresse" "modifadresse"
] = f"""<a class="stdlink" href="{ ] = f"""<a class="stdlink" href="{
@ -242,36 +211,33 @@ def ficheEtud(etudid=None):
info["modifadresse"] = "" info["modifadresse"] = ""
# Groupes: # Groupes:
inscription_courante = etud.inscription_courante()
sco_groups.etud_add_group_infos( sco_groups.etud_add_group_infos(
info, info,
info["cursem"]["formsemestre_id"] if info["cursem"] else None, inscription_courante.formsemestre.id if inscription_courante else None,
only_to_show=True, only_to_show=True,
) )
# Parcours de l'étudiant # Parcours de l'étudiant
if info["sems"]: last_formsemestre = None
info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"] inscriptions = etud.inscriptions()
else: info["last_formsemestre_id"] = (
info["last_formsemestre_id"] = "" inscriptions[0].formsemestre.id if inscriptions else ""
)
sem_info = {} sem_info = {}
for sem in info["sems"]: for inscription in inscriptions:
formsemestre: FormSemestre = db.session.get( formsemestre = inscription.formsemestre
FormSemestre, sem["formsemestre_id"] if inscription.etat != scu.INSCRIT:
)
if sem["ins"]["etat"] != scu.INSCRIT:
descr, _ = etud_descr_situation_semestre( descr, _ = etud_descr_situation_semestre(
etudid, etudid,
formsemestre, formsemestre,
info["ne"], etud.e,
show_date_inscr=False, show_date_inscr=False,
) )
grlink = f"""<span class="fontred">{descr["situation"]}</span>""" grlink = f"""<span class="fontred">{descr["situation"]}</span>"""
else: else:
e = {"etudid": etudid} e = {"etudid": etudid}
sco_groups.etud_add_group_infos( sco_groups.etud_add_group_infos(e, formsemestre.id, only_to_show=True)
e,
sem["formsemestre_id"],
only_to_show=True,
)
grlinks = [] grlinks = []
for partition in e["partitions"].values(): for partition in e["partitions"].values():
@ -289,16 +255,16 @@ def ficheEtud(etudid=None):
) )
grlink = ", ".join(grlinks) grlink = ", ".join(grlinks)
# infos ajoutées au semestre dans le parcours (groupe, menu) # infos ajoutées au semestre dans le parcours (groupe, menu)
menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"]) menu = _menu_scolarite(current_user, formsemestre, etudid, inscription.etat)
if menu: if menu:
sem_info[sem["formsemestre_id"]] = ( sem_info[formsemestre.id] = (
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>" "<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
) )
else: else:
sem_info[sem["formsemestre_id"]] = grlink sem_info[formsemestre.id] = grlink
if info["sems"]: if inscriptions:
Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"]) Se = sco_cursus.get_situation_etud_cursus(info, info["last_formsemestre_id"])
info["liste_inscriptions"] = formsemestre_recap_parcours_table( info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se, Se,
etudid, etudid,
@ -318,20 +284,19 @@ def ficheEtud(etudid=None):
</span> </span>
""" """
) )
last_formsemestre: FormSemestre = db.session.get( last_formsemestre: FormSemestre = inscriptions[0].formsemestre
FormSemestre, info["sems"][0]["formsemestre_id"]
)
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2: if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
info[ info[
"link_bul_pdf" "link_bul_pdf"
] += f""" ] += f"""
<span class="link_bul_pdf"> <span class="link_bul_pdf">
<a class="stdlink" href="{ <a class="stdlink" href="{
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id) url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
}">Visualiser les compétences BUT</a> }">Visualiser les compétences BUT</a>
</span> </span>
""" """
if authuser.has_permission(Permission.EtudInscrit): if current_user.has_permission(Permission.EtudInscrit):
info[ info[
"link_inscrire_ailleurs" "link_inscrire_ailleurs"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{ ] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
@ -348,8 +313,8 @@ def ficheEtud(etudid=None):
info["link_inscrire_ailleurs"] = "" info["link_inscrire_ailleurs"] = ""
else: else:
# non inscrit # non inscrit
l = [f"""<p><b>Étudiant{info["ne"]} non inscrit{info["ne"]}"""] l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
if authuser.has_permission(Permission.EtudInscrit): if current_user.has_permission(Permission.EtudInscrit):
l.append( l.append(
f"""<a href="{ f"""<a href="{
url_for("notes.formsemestre_inscription_with_modules_form", url_for("notes.formsemestre_inscription_with_modules_form",
@ -362,44 +327,50 @@ def ficheEtud(etudid=None):
info["link_inscrire_ailleurs"] = "" info["link_inscrire_ailleurs"] = ""
# Liste des annotations # Liste des annotations
alist = [] annotations_list = []
annos = sco_etud.etud_annotations_list(cnx, args={"etudid": etudid}) annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by(
for a in annos: sa.desc(EtudAnnotation.date)
if not sco_permissions_check.can_suppress_annotation(a["id"]): )
a["dellink"] = "" for annot in annotations:
else: del_link = (
a["dellink"] = ( f"""<td class="annodel"><a href="{
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' url_for("scolar.doSuppressAnnotation",
% ( scodoc_dept=g.scodoc_dept, etudid=etudid, annotation_id=annot.id)}">{
etudid, scu.icontag(
a["id"],
scu.icontag(
"delete_img", "delete_img",
border="0", border="0",
alt="suppress", alt="suppress",
title="Supprimer cette annotation", title="Supprimer cette annotation",
),
) )
) }</a></td>"""
author = sco_users.user_info(a["author"]) if sco_permissions_check.can_suppress_annotation(annot.id)
alist.append( else ""
f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} : )
</span><span class="annoc">{a['comment']}</span></td>{a['dellink']}</tr>
author = User.query.filter_by(user_name=annot.author).first()
annotations_list.append(
f"""<tr><td><span class="annodate">Le {annot.date.strftime("%d/%m/%Y") if annot.date else "?"}
par {author.get_prenomnom() if author else "?"} :
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
""" """
) )
info["liste_annotations"] = "\n".join(alist) info["liste_annotations"] = "\n".join(annotations_list)
# fiche admission # fiche admission
has_adm_notes = ( infos_admission = _infos_admission(etud, restrict_etud_data)
info["math"] or info["physique"] or info["anglais"] or info["francais"] has_adm_notes = any(
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
) )
has_bac_info = ( has_bac_info = any(
info["bac"] infos_admission[k]
or info["specialite"] for k in (
or info["annee_bac"] "bac_specialite",
or info["rapporteur"] "annee_bac",
or info["commentaire"] "rapporteur",
or info["classement"] "commentaire",
or info["type_admission"] "classement",
"type_admission",
"rap",
)
) )
if has_bac_info or has_adm_notes: if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission --> adm_tmpl = """<!-- Donnees admission -->
@ -411,7 +382,7 @@ def ficheEtud(etudid=None):
<tr><th>Bac</th><th>Année</th><th>Rg</th> <tr><th>Bac</th><th>Année</th><th>Rg</th>
<th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr> <th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
<tr> <tr>
<td>%(bac)s (%(specialite)s)</td> <td>%(bac_specialite)s</td>
<td>%(annee_bac)s </td> <td>%(annee_bac)s </td>
<td>%(classement)s</td> <td>%(classement)s</td>
<td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td> <td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
@ -419,27 +390,31 @@ def ficheEtud(etudid=None):
</table> </table>
""" """
adm_tmpl += """ adm_tmpl += """
<div>Bac %(bac)s (%(specialite)s) obtenu en %(annee_bac)s </div> <div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="ilycee">%(ilycee)s</div>""" <div class="info_lycee">%(info_lycee)s</div>"""
if info["type_admission"] or info["classement"]: if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">""" adm_tmpl += """<div class="vadmission">"""
if info["type_admission"]: if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """ adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if info["classement"]: if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>""" adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if info["type_admission"] or info["classement"]: if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>" adm_tmpl += "</div>"
if info["rap"]: if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>""" adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>""" adm_tmpl += """</div>"""
else: else:
adm_tmpl = "" # pas de boite "info admission" adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % info info["adm_data"] = adm_tmpl % infos_admission
# Fichiers archivés: # Fichiers archivés:
info["fichiers_archive_htm"] = ( info["fichiers_archive_htm"] = (
'<div class="fichetitre">Fichiers associés</div>' ""
+ sco_archives_etud.etud_list_archives_html(etud) if restrict_etud_data
else (
'<div class="fichetitre">Fichiers associés</div>'
+ sco_archives_etud.etud_list_archives_html(etud)
)
) )
# Devenir de l'étudiant: # Devenir de l'étudiant:
@ -455,18 +430,16 @@ def ficheEtud(etudid=None):
if has_debouche: if has_debouche:
info[ info[
"debouche_html" "debouche_html"
] = """<div id="fichedebouche" data-readonly="%s" data-etudid="%s"> ] = f"""<div id="fichedebouche"
data-readonly="{suivi_readonly}"
data-etudid="{info['etudid']}">
<span class="debouche_tit">Devenir:</span> <span class="debouche_tit">Devenir:</span>
<div><form> <div><form>
<ul class="listdebouches"> <ul class="listdebouches">
%s {link_add_suivi}
</ul> </ul>
</form></div> </form></div>
</div>""" % ( </div>"""
suivi_readonly,
info["etudid"],
link_add_suivi,
)
else: else:
info["debouche_html"] = "" # pas de boite "devenir" info["debouche_html"] = "" # pas de boite "devenir"
# #
@ -492,70 +465,92 @@ def ficheEtud(etudid=None):
else: else:
info["groupes_row"] = "" info["groupes_row"] = ""
info["menus_etud"] = menus_etud(etudid) info["menus_etud"] = menus_etud(etudid)
if info["boursier"]: if info["boursier"] and not restrict_etud_data:
info["bourse_span"] = """<span class="boursier">boursier</span>""" info["bourse_span"] = """<span class="boursier">boursier</span>"""
else: else:
info["bourse_span"] = "" info["bourse_span"] = ""
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche... # Liens vers compétences BUT
# info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid) if last_formsemestre and last_formsemestre.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
# XXX dev info[
info["but_cursus_mkup"] = "" "but_cursus_mkup"
if info["sems"]: ] = f"""
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"]) <div class="section_but">
if last_sem.formation.is_apc(): {render_template(
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation) "but/cursus_etud.j2",
info[ cursus=but_cursus,
"but_cursus_mkup" scu=scu,
] = f""" )}
<div class="section_but"> <div class="link_validation_rcues">
{render_template( <a class="stdlink" href="{url_for("notes.validation_rcues",
"but/cursus_etud.j2", scodoc_dept=g.scodoc_dept, etudid=etudid,
cursus=but_cursus, formsemestre_id=last_formsemestre.id)}"
scu=scu, title="Visualiser les compétences BUT"
)} >
<div class="link_validation_rcues"> <img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
<a href="{url_for("notes.validation_rcues", <div>Compétences BUT</div>
scodoc_dept=g.scodoc_dept, etudid=etudid, </a>
formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
</a>
</div>
</div> </div>
""" </div>
"""
else:
info["but_cursus_mkup"] = ""
tmpl = """<div class="menus_etud">%(menus_etud)s</div> adresse_template = (
<div class="ficheEtud" id="ficheEtud"><table> ""
if restrict_etud_data
else """
<!-- Adresse -->
<div class="ficheadresse" id="ficheadresse">
<table>
<tr>
<td class="fichetitre2">Adresse :</td>
<td> %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s
%(modifadresse)s
%(telephones)s
</td>
</tr>
</table>
</div>
"""
)
info_naissance = (
f"""<tr><td class="fichetitre2">Né{etud.e} le :</td><td>{info["info_naissance"]}</td></tr>"""
if info["info_naissance"]
else ""
)
situation_template = (
f"""
<div class="fichesituation">
<div class="fichetablesitu">
<table>
<tr><td class="fichetitre2">Situation :</td><td>%(situation)s %(bourse_span)s</td></tr>
%(groupes_row)s
{info_naissance}
</table>
"""
+ adresse_template
+ """
</div>
</div>
"""
)
tmpl = (
"""<div class="menus_etud">%(menus_etud)s</div>
<div class="fiche_etud" id="fiche_etud"><table>
<tr><td> <tr><td>
<h2>%(nomprenom)s (%(inscription)s)</h2> <h2>%(nomprenom)s (%(inscription)s)</h2>
%(etat_civil)s %(etat_civil)s
<span>%(emaillink)s</span> <span>%(email_link)s</span>
</td><td class="photocell"> </td><td class="photocell">
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a> <a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
</td></tr></table> </td></tr></table>
"""
<div class="fichesituation"> + situation_template
<div class="fichetablesitu"> + """
<table>
<tr><td class="fichetitre2">Situation :</td><td>%(situation)s %(bourse_span)s</td></tr>
%(groupes_row)s
<tr><td class="fichetitre2">%(ne)s le :</td><td>%(info_naissance)s</td></tr>
</table>
<!-- Adresse -->
<div class="ficheadresse" id="ficheadresse">
<table><tr>
<td class="fichetitre2">Adresse :</td><td> %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s
%(modifadresse)s
%(telephones)s
</td></tr></table>
</div>
</div>
</div>
%(inscriptions_mkup)s %(inscriptions_mkup)s
@ -595,8 +590,9 @@ def ficheEtud(etudid=None):
</div> </div>
""" """
)
header = html_sco_header.sco_header( header = html_sco_header.sco_header(
page_title="Fiche étudiant %(prenom)s %(nom)s" % info, page_title=f"Fiche étudiant {etud.nomprenom}",
cssstyles=[ cssstyles=[
"libjs/jQuery-tagEditor/jquery.tag-editor.css", "libjs/jQuery-tagEditor/jquery.tag-editor.css",
"css/jury_but.css", "css/jury_but.css",
@ -614,6 +610,92 @@ def ficheEtud(etudid=None):
return header + tmpl % info + html_sco_header.sco_footer() return header + tmpl % info + html_sco_header.sco_footer()
def _format_adresse(adresse: Adresse | None) -> dict:
"""{ "telephonestr" : ..., "telephonemobilestr" : ... } (formats html)"""
d = {
"telephonestr": ("<b>Tél.:</b> " + scu.format_telephone(adresse.telephone))
if (adresse and adresse.telephone)
else "",
"telephonemobilestr": (
"<b>Mobile:</b> " + scu.format_telephone(adresse.telephonemobile)
)
if (adresse and adresse.telephonemobile)
else "",
# e-mail:
"email_link": ", ".join(
[
f"""<a class="stdlink" href="mailto:{m}">{m}</a>"""
for m in [adresse.email, adresse.emailperso]
if m
]
)
if adresse and (adresse.email or adresse.emailperso)
else "",
"domicile": (adresse.domicile or "")
if adresse
and (adresse.domicile or adresse.codepostaldomicile or adresse.villedomicile)
else "<em>inconnue</em>",
"paysdomicile": f"{sco_etud.format_pays(adresse.paysdomicile)}"
if adresse and adresse.paysdomicile
else "",
}
d["telephones"] = (
f"<br>{d['telephonestr']} &nbsp;&nbsp; {d['telephonemobilestr']}"
if adresse and (adresse.telephone or adresse.telephonemobile)
else ""
)
return d
def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
"""dict with adminission data, restricted or not"""
# info sur rapporteur et son commentaire
rap = ""
if not restrict_etud_data:
if etud.admission.rapporteur or etud.admission.commentaire:
rap = "Note du rapporteur"
if etud.admission.rapporteur:
rap += f" ({etud.admission.rapporteur})"
rap += ": "
if etud.admission.commentaire:
rap += f"<em>{etud.admission.commentaire}</em>"
# nom du lycée
if restrict_etud_data:
info_lycee = ""
elif etud.admission.nomlycee:
info_lycee = "Lycée " + sco_etud.format_lycee(etud.admission.nomlycee)
if etud.admission.villelycee:
info_lycee += f" ({etud.admission.villelycee})"
info_lycee += "<br>"
elif etud.admission.codelycee:
info_lycee = sco_etud.format_lycee_from_code(etud.admission.codelycee)
else:
info_lycee = ""
return {
# infos accessibles à tous:
"bac_specialite": f"{etud.admission.bac or ''}{(' '+(etud.admission.specialite or '')) if etud.admission.specialite else ''}",
"annee_bac": etud.admission.annee_bac or "",
# infos protégées par ViewEtudData:
"info_lycee": info_lycee,
"rapporteur": etud.admission.rapporteur if not restrict_etud_data else "",
"rap": rap,
"commentaire": (etud.admission.commentaire or "")
if not restrict_etud_data
else "",
"classement": (etud.admission.classement or "")
if not restrict_etud_data
else "",
"type_admission": (etud.admission.type_admission or "")
if not restrict_etud_data
else "",
"math": (etud.admission.math or "") if not restrict_etud_data else "",
"physique": (etud.admission.physique or "") if not restrict_etud_data else "",
"anglais": (etud.admission.anglais or "") if not restrict_etud_data else "",
"francais": (etud.admission.francais or "") if not restrict_etud_data else "",
}
def menus_etud(etudid): def menus_etud(etudid):
"""Menu etudiant (operations sur l'etudiant)""" """Menu etudiant (operations sur l'etudiant)"""
authuser = current_user authuser = current_user
@ -623,7 +705,7 @@ def menus_etud(etudid):
menuEtud = [ menuEtud = [
{ {
"title": etud["nomprenom"], "title": etud["nomprenom"],
"endpoint": "scolar.ficheEtud", "endpoint": "scolar.fiche_etud",
"args": {"etudid": etud["etudid"]}, "args": {"etudid": etud["etudid"]},
"enabled": True, "enabled": True,
"helpmsg": "Fiche étudiant", "helpmsg": "Fiche étudiant",
@ -638,7 +720,8 @@ def menus_etud(etudid):
"title": "Changer les données identité/admission", "title": "Changer les données identité/admission",
"endpoint": "scolar.etudident_edit_form", "endpoint": "scolar.etudident_edit_form",
"args": {"etudid": etud["etudid"]}, "args": {"etudid": etud["etudid"]},
"enabled": authuser.has_permission(Permission.EtudInscrit), "enabled": authuser.has_permission(Permission.EtudInscrit)
and authuser.has_permission(Permission.ViewEtudData),
}, },
{ {
"title": "Copier dans un autre département...", "title": "Copier dans un autre département...",
@ -669,38 +752,39 @@ def etud_info_html(etudid, with_photo="1", debug=False):
"""An HTML div with basic information and links about this etud. """An HTML div with basic information and links about this etud.
Used for popups information windows. Used for popups information windows.
""" """
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request() formsemestre_id = retreive_formsemestre_from_request()
with_photo = int(with_photo) with_photo = int(with_photo)
etuds = sco_etud.get_etud_info(filled=True) etud = Identite.get_etud(etudid)
if etuds:
etud = etuds[0]
else:
abort(404, "etudiant inconnu")
photo_html = sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"])
# experimental: may be too slow to be here
code_cursus, _ = sco_report.get_code_cursus_etud(etud, prefix="S", separator=", ")
bac = sco_bac.Baccalaureat(etud["bac"], etud["specialite"]) photo_html = etud.photo_html(title="fiche de " + etud.nomprenom)
code_cursus, _ = sco_report.get_code_cursus_etud(
etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", "
)
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
bac_abbrev = bac.abbrev() bac_abbrev = bac.abbrev()
H = f"""<div class="etud_info_div"> H = f"""<div class="etud_info_div">
<div class="eid_left"> <div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{ <div class="eid_nom"><div><a class="stdlink" target="_blank" href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{etud["nomprenom"]}</a></div></div> }">{etud.nomprenom}</a></div></div>
<div class="eid_info eid_bac">Bac: <span class="eid_bac">{bac_abbrev}</span></div> <div class="eid_info eid_bac">Bac: <span class="eid_bac">{bac_abbrev}</span></div>
<div class="eid_info eid_parcours">{code_cursus}</div> <div class="eid_info eid_parcours">{code_cursus}</div>
""" """
# Informations sur l'etudiant dans le semestre courant: # Informations sur l'etudiant dans le semestre courant:
sem = None
if formsemestre_id: # un semestre est spécifié par la page if formsemestre_id: # un semestre est spécifié par la page
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
elif etud["cursem"]: # le semestre "en cours" pour l'étudiant else:
sem = etud["cursem"] # le semestre "en cours" pour l'étudiant
if sem: inscription_courante = etud.inscription_courante()
groups = sco_groups.get_etud_groups(etudid, formsemestre_id) formsemestre = (
inscription_courante.formsemestre if inscription_courante else None
)
if formsemestre:
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
grc = sco_groups.listgroups_abbrev(groups) grc = sco_groups.listgroups_abbrev(groups)
H += f"""<div class="eid_info">En <b>S{sem["semestre_id"]}</b>: {grc}</div>""" H += f"""<div class="eid_info">En <b>S{formsemestre.semestre_id}</b>: {grc}</div>"""
H += "</div>" # fin partie gauche (eid_left) H += "</div>" # fin partie gauche (eid_left)
if with_photo: if with_photo:
H += '<span class="eid_right">' + photo_html + "</span>" H += '<span class="eid_right">' + photo_html + "</span>"

View File

@ -24,7 +24,7 @@ _SCO_PERMISSIONS = (
(1 << 10, "EditAllNotes", "Modifier toutes les notes"), (1 << 10, "EditAllNotes", "Modifier toutes les notes"),
(1 << 11, "EditAllEvals", "Modifier toutes les évaluations"), (1 << 11, "EditAllEvals", "Modifier toutes les évaluations"),
(1 << 12, "EditFormSemestre", "Mettre en place une formation (créer un semestre)"), (1 << 12, "EditFormSemestre", "Mettre en place une formation (créer un semestre)"),
(1 << 13, "AbsChange", "Saisir des absences"), (1 << 13, "AbsChange", "Saisir des absences ou justificatifs"),
(1 << 14, "AbsAddBillet", "Saisir des billets d'absences"), (1 << 14, "AbsAddBillet", "Saisir des billets d'absences"),
# changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
(1 << 15, "EtudChangeAdr", "Changer les adresses d'étudiants"), (1 << 15, "EtudChangeAdr", "Changer les adresses d'étudiants"),
@ -37,7 +37,11 @@ _SCO_PERMISSIONS = (
# aussi pour demissions, diplomes: # aussi pour demissions, diplomes:
(1 << 17, "EtudInscrit", "Inscrire des étudiants"), (1 << 17, "EtudInscrit", "Inscrire des étudiants"),
# aussi pour archives: # aussi pour archives:
(1 << 18, "EtudAddAnnotations", "Éditer les annotations"), (
1 << 18,
"EtudAddAnnotations",
"Éditer les annotations (et fichiers) sur étudiants",
),
# inutilisée (1 << 19, "ScoEntrepriseView", "Voir la section 'entreprises'"), # inutilisée (1 << 19, "ScoEntrepriseView", "Voir la section 'entreprises'"),
# inutilisée (1 << 20, "EntrepriseChange", "Modifier les entreprises"), # inutilisée (1 << 20, "EntrepriseChange", "Modifier les entreprises"),
# XXX inutilisée ? (1 << 21, "EditPVJury", "Éditer les PV de jury"), # XXX inutilisée ? (1 << 21, "EditPVJury", "Éditer les PV de jury"),
@ -55,10 +59,15 @@ _SCO_PERMISSIONS = (
"Exporter les données de l'application relations entreprises", "Exporter les données de l'application relations entreprises",
), ),
(1 << 29, "UsersChangeCASId", "Paramétrer l'id CAS"), (1 << 29, "UsersChangeCASId", "Paramétrer l'id CAS"),
(1 << 30, "ViewEtudData", "Accéder aux données personnelles des étudiants"),
# #
# XXX inutilisée ? (1 << 40, "EtudChangePhoto", "Modifier la photo d'un étudiant"), # XXX inutilisée ? (1 << 40, "EtudChangePhoto", "Modifier la photo d'un étudiant"),
# Permissions du module Assiduité) # Permissions du module Assiduité)
(1 << 50, "AbsJustifView", "Visualisation des fichiers justificatifs"), (
1 << 50,
"AbsJustifView",
"Visualisation du détail des justificatifs (motif, fichiers)",
),
# Attention: les permissions sont codées sur 64 bits. # Attention: les permissions sont codées sur 64 bits.
) )

View File

@ -38,20 +38,19 @@ from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.codes_cursus import code_semestre_validant, code_semestre_attente from app.scodoc.codes_cursus import code_semestre_validant, code_semestre_attente
import sco_version
def etud_get_poursuite_info(sem, etud): def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
"""{ 'nom' : ..., 'semlist' : [ { 'semestre_id': , 'moy' : ... }, {}, ...] }""" """{ 'nom' : ..., 'semlist' : [ { 'semestre_id': , 'moy' : ... }, {}, ...] }"""
I = {} infos = {}
I.update(etud) # copie nom, prenom, civilite, ... infos.update(etud) # copie nom, prenom, civilite, ...
# Now add each semester, starting from the first one # Now add each semester, starting from the first one
semlist = [] semlist = []
@ -92,25 +91,28 @@ def etud_get_poursuite_info(sem, etud):
for ue in ues: # on parcourt chaque UE for ue in ues: # on parcourt chaque UE
for modimpl in modimpls: # dans chaque UE les modules for modimpl in modimpls: # dans chaque UE les modules
if modimpl["module"]["ue_id"] == ue["ue_id"]: if modimpl["module"]["ue_id"] == ue["ue_id"]:
codeModule = modimpl["module"]["code"] or "" code_module = modimpl["module"]["code"] or ""
noteModule = scu.fmt_note( note_module = scu.fmt_note(
nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
) )
if noteModule != "NI": # si étudiant inscrit au module # si étudiant inscrit au module, sauf BUT
if (note_module != "NI") and not nt.is_apc:
if nt.mod_rangs is not None: if nt.mod_rangs is not None:
rangModule = nt.mod_rangs[modimpl["moduleimpl_id"]][ rang_module = nt.mod_rangs[
0 modimpl["moduleimpl_id"]
][etudid] ][0][etudid]
else: else:
rangModule = "" rang_module = ""
modules.append([codeModule, noteModule]) modules.append([code_module, note_module])
rangs.append(["rang_" + codeModule, rangModule]) rangs.append(["rang_" + code_module, rang_module])
# Absences # Absences
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem) nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
if ( # En BUT, prend tout, sinon ne prend que les semestre validés par le jury
if nt.is_apc or (
dec dec
and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent # not sem_descr pour ne prendre que le semestre validé le plus récent:
and not sem_descr
and ( and (
code_semestre_validant(dec["code"]) code_semestre_validant(dec["code"])
or code_semestre_attente(dec["code"]) or code_semestre_attente(dec["code"])
@ -128,9 +130,8 @@ def etud_get_poursuite_info(sem, etud):
("AbsNonJust", nbabs - nbabsjust), ("AbsNonJust", nbabs - nbabsjust),
("AbsJust", nbabsjust), ("AbsJust", nbabsjust),
] ]
d += ( # ajout des 2 champs notes des modules et classement dans chaque module
moy_ues + rg_ues + modules + rangs d += moy_ues + rg_ues + modules + rangs
) # ajout des 2 champs notes des modules et classement dans chaque module
sem_descr = collections.OrderedDict(d) sem_descr = collections.OrderedDict(d)
if not sem_descr: if not sem_descr:
sem_descr = collections.OrderedDict( sem_descr = collections.OrderedDict(
@ -147,13 +148,14 @@ def etud_get_poursuite_info(sem, etud):
sem_descr["semestre_id"] = sem_id sem_descr["semestre_id"] = sem_id
semlist.append(sem_descr) semlist.append(sem_descr)
I["semlist"] = semlist infos["semlist"] = semlist
return I return infos
def _flatten_info(info): def _flatten_info(info):
# met la liste des infos semestres "a plat" """met la liste des infos semestres "a plat"
# S1_moy, S1_rang, ..., S2_moy, ... S1_moy, S1_rang, ..., S2_moy, ...
"""
ids = [] ids = []
for s in info["semlist"]: for s in info["semlist"]:
for k, v in s.items(): for k, v in s.items():
@ -164,7 +166,7 @@ def _flatten_info(info):
return ids return ids
def _getEtudInfoGroupes(group_ids, etat=None): def _get_etud_info_groupes(group_ids, etat=None):
"""liste triée d'infos (dict) sur les etudiants du groupe indiqué. """liste triée d'infos (dict) sur les etudiants du groupe indiqué.
Attention: lent, car plusieurs requetes SQL par etudiant ! Attention: lent, car plusieurs requetes SQL par etudiant !
""" """
@ -181,17 +183,17 @@ def _getEtudInfoGroupes(group_ids, etat=None):
def formsemestre_poursuite_report(formsemestre_id, fmt="html"): def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
"""Table avec informations "poursuite" """ """Table avec informations "poursuite" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = _getEtudInfoGroupes([sco_groups.get_default_group(formsemestre_id)]) etuds = _get_etud_info_groupes([sco_groups.get_default_group(formsemestre_id)])
infos = [] infos = []
ids = [] ids = []
for etud in etuds: for etud in etuds:
fiche_url = url_for( fiche_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
) )
etud["_nom_target"] = fiche_url etud["_nom_target"] = fiche_url
etud["_prenom_target"] = fiche_url etud["_prenom_target"] = fiche_url
etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) etud["_nom_td_attrs"] = f"""id="{etud['etudid']}" class="etudinfo" """
info = etud_get_poursuite_info(sem, etud) info = etud_get_poursuite_info(sem, etud)
idd = _flatten_info(info) idd = _flatten_info(info)
# On recupere la totalite des UEs dans ids # On recupere la totalite des UEs dans ids

View File

@ -147,6 +147,7 @@ def get_preference(name, formsemestre_id=None, dept_id=None):
"""Returns value of named preference. """Returns value of named preference.
All preferences have a sensible default value, so this All preferences have a sensible default value, so this
function always returns a usable value for all defined preferences names. function always returns a usable value for all defined preferences names.
If dept_id is None, use current dept (g.scodoc_dept_id)
""" """
return get_base_preferences(dept_id=dept_id).get(formsemestre_id, name) return get_base_preferences(dept_id=dept_id).get(formsemestre_id, name)
@ -541,18 +542,6 @@ class BasePreferences:
"category": "abs", "category": "abs",
}, },
), ),
(
"abs_notify_max_freq",
{
"initvalue": 7,
"title": "Fréquence maximale de notification",
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
"size": 4,
"type": "int",
"convert_numbers": True,
"category": "abs",
},
),
( (
"abs_notify_abs_threshold", "abs_notify_abs_threshold",
{ {
@ -710,11 +699,23 @@ class BasePreferences:
"size": 10, "size": 10,
"title": "Seuil d'alerte des absences", "title": "Seuil d'alerte des absences",
"type": "int", "type": "int",
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )", "explanation": "Nombres d'absences limite avant alerte (utilisation de l'unité métrique ↑ )",
"category": "assi", "category": "assi",
"only_global": True, "only_global": True,
}, },
), ),
(
"abs_notify_max_freq",
{
"initvalue": 7,
"title": "Fréquence maximale de notification",
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
"size": 4,
"type": "int",
"convert_numbers": True,
"category": "abs",
},
),
# portal # portal
( (
"portal_url", "portal_url",

View File

@ -144,7 +144,7 @@ def pvjury_table(
"code_nip": e["identite"]["code_nip"], "code_nip": e["identite"]["code_nip"],
"nomprenom": e["identite"]["nomprenom"], "nomprenom": e["identite"]["nomprenom"],
"_nomprenom_target": url_for( "_nomprenom_target": url_for(
"scolar.ficheEtud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=e["identite"]["etudid"], etudid=e["identite"]["etudid"],
), ),
@ -351,7 +351,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
# PV pour ce seul étudiant: # PV pour ce seul étudiant:
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
etuddescr = f"""<a class="discretelink" href="{ etuddescr = f"""<a class="discretelink" href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{etud.nomprenom}</a>""" }">{etud.nomprenom}</a>"""
etudids = [etudid] etudids = [etudid]
else: else:

View File

@ -1017,34 +1017,60 @@ EXP_LIC = re.compile(r"licence", re.I)
EXP_LPRO = re.compile(r"professionnelle", re.I) EXP_LPRO = re.compile(r"professionnelle", re.I)
def _codesem(sem, short=True, prefix=""): def _code_sem(
semestre_id: int, titre: str, mois_debut: int, short=True, prefix=""
) -> str:
"code semestre: S1 ou S1d" "code semestre: S1 ou S1d"
idx = sem["semestre_id"] idx = semestre_id
# semestre décalé ? # semestre décalé ?
# les semestres pairs normaux commencent entre janvier et mars # les semestres pairs normaux commencent entre janvier et mars
# les impairs normaux entre aout et decembre # les impairs normaux entre aout et decembre
d = "" d = ""
if idx and idx > 0 and sem["date_debut"]: if idx > 0:
mois_debut = int(sem["date_debut"].split("/")[1])
if (idx % 2 and mois_debut < 3) or (idx % 2 == 0 and mois_debut >= 8): if (idx % 2 and mois_debut < 3) or (idx % 2 == 0 and mois_debut >= 8):
d = "d" d = "d"
if idx == -1: if idx == -1:
if short: if short:
idx = "Autre " idx = "Autre "
else: else:
idx = sem["titre"] + " " idx = titre + " "
idx = EXP_LPRO.sub("pro.", idx) idx = EXP_LPRO.sub("pro.", idx)
idx = EXP_LIC.sub("Lic.", idx) idx = EXP_LIC.sub("Lic.", idx)
prefix = "" # indique titre au lieu de Sn prefix = "" # indique titre au lieu de Sn
return "%s%s%s" % (prefix, idx, d) return prefix + str(idx) + d
def get_code_cursus_etud(etud, prefix="", separator=""): def _code_sem_formsemestre(formsemestre: FormSemestre, short=True, prefix="") -> str:
"code semestre: S1 ou S1d"
titre = formsemestre.titre
mois_debut = formsemestre.date_debut.month
semestre_id = formsemestre.semestre_id
return _code_sem(semestre_id, titre, mois_debut, short=short, prefix=prefix)
def _code_sem_dict(sem, short=True, prefix="") -> str:
"code semestre: S1 ou S1d, à parit d'un dict (sem ScoDoc 7)"
titre = sem["titre"]
mois_debut = int(sem["date_debut"].split("/")[1]) if sem["date_debut"] else 0
semestre_id = sem["semestre_id"]
return _code_sem(semestre_id, titre, mois_debut, short=short, prefix=prefix)
def get_code_cursus_etud(
etudid: int,
sems: list[dict] = None,
formsemestres: list[FormSemestre] | None = None,
prefix="",
separator="",
) -> tuple[str, dict]:
"""calcule un code de cursus (parcours) pour un etudiant """calcule un code de cursus (parcours) pour un etudiant
exemples: exemples:
1234A pour un etudiant ayant effectué S1, S2, S3, S4 puis diplome 1234A pour un etudiant ayant effectué S1, S2, S3, S4 puis diplome
12D pour un étudiant en S1, S2 puis démission en S2 12D pour un étudiant en S1, S2 puis démission en S2
12R pour un etudiant en S1, S2 réorienté en fin de S2 12R pour un etudiant en S1, S2 réorienté en fin de S2
On peut passer soir la liste des semestres dict (anciennes fonctions ScoDoc7)
soit la liste des FormSemestre.
Construit aussi un dict: { semestre_id : decision_jury | None } Construit aussi un dict: { semestre_id : decision_jury | None }
""" """
# Nota: approche plus moderne: # Nota: approche plus moderne:
@ -1054,36 +1080,42 @@ def get_code_cursus_etud(etud, prefix="", separator=""):
# #
p = [] p = []
decisions_jury = {} decisions_jury = {}
# élimine les semestres spéciaux hors cursus (LP en 1 sem., ...)
sems = [s for s in etud["sems"] if s["semestre_id"] >= 0]
i = len(sems) - 1
while i >= 0:
s = sems[i] # 'sems' est a l'envers, du plus recent au plus ancien
s_formsemestre = FormSemestre.query.get_or_404(s["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(s_formsemestre)
p.append(_codesem(s, prefix=prefix)) if formsemestres is None:
formsemestres = [
FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in (sems or [])
]
# élimine les semestres spéciaux hors cursus (LP en 1 sem., ...)
formsemestres = [s for s in formsemestres if s.semestre_id >= 0]
i = len(formsemestres) - 1
while i >= 0:
# 'sems' est a l'envers, du plus recent au plus ancien
formsemestre = formsemestres[i]
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
p.append(_code_sem_formsemestre(formsemestre, prefix=prefix))
# code decisions jury de chaque semestre: # code decisions jury de chaque semestre:
if nt.get_etud_etat(etud["etudid"]) == "D": if nt.get_etud_etat(etudid) == "D":
decisions_jury[s["semestre_id"]] = "DEM" decisions_jury[formsemestre.semestre_id] = "DEM"
else: else:
dec = nt.get_etud_decision_sem(etud["etudid"]) dec = nt.get_etud_decision_sem(etudid)
if not dec: if not dec:
decisions_jury[s["semestre_id"]] = "" decisions_jury[formsemestre.semestre_id] = ""
else: else:
decisions_jury[s["semestre_id"]] = dec["code"] decisions_jury[formsemestre.semestre_id] = dec["code"]
# code etat dans le code_cursus sur dernier semestre seulement # code etat dans le code_cursus sur dernier semestre seulement
if i == 0: if i == 0:
# Démission # Démission
if nt.get_etud_etat(etud["etudid"]) == "D": if nt.get_etud_etat(etudid) == "D":
p.append(":D") p.append(":D")
else: else:
dec = nt.get_etud_decision_sem(etud["etudid"]) dec = nt.get_etud_decision_sem(etudid)
if dec and dec["code"] in codes_cursus.CODES_SEM_REO: if dec and dec["code"] in codes_cursus.CODES_SEM_REO:
p.append(":R") p.append(":R")
if ( if (
dec dec
and s["semestre_id"] == nt.parcours.NB_SEM and formsemestre.semestre_id == nt.parcours.NB_SEM
and code_semestre_validant(dec["code"]) and code_semestre_validant(dec["code"])
): ):
p.append(":A") p.append(":A")
@ -1176,14 +1208,16 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
) = tsp_etud_list(formsemestre_id, only_primo=only_primo) ) = tsp_etud_list(formsemestre_id, only_primo=only_primo)
codes_etuds = collections.defaultdict(list) codes_etuds = collections.defaultdict(list)
for etud in etuds: for etud in etuds:
etud["code_cursus"], etud["decisions_jury"] = get_code_cursus_etud(etud) etud["code_cursus"], etud["decisions_jury"] = get_code_cursus_etud(
etud["etudid"], sems=etud["sems"]
)
codes_etuds[etud["code_cursus"]].append(etud) codes_etuds[etud["code_cursus"]].append(etud)
fiche_url = url_for( fiche_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
) )
etud["_nom_target"] = fiche_url etud["_nom_target"] = fiche_url
etud["_prenom_target"] = fiche_url etud["_prenom_target"] = fiche_url
etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) etud["_nom_td_attrs"] = f'''id="{etud['etudid']}" class="etudinfo"'''
titles = { titles = {
"parcours": "Code cursus", "parcours": "Code cursus",
@ -1461,7 +1495,7 @@ def graph_cursus(
else: else:
modalite = "" modalite = ""
label = "%s%s\\n%d/%s - %d/%s\\n%d" % ( label = "%s%s\\n%d/%s - %d/%s\\n%d" % (
_codesem(s, short=False, prefix="S"), _code_sem_dict(s, short=False, prefix="S"),
modalite, modalite,
s["mois_debut_ord"], s["mois_debut_ord"],
s["annee_debut"][2:], s["annee_debut"][2:],

View File

@ -13,8 +13,9 @@ SCO_ROLES_DEFAULTS = {
p.EnsView, p.EnsView,
p.EtudAddAnnotations, p.EtudAddAnnotations,
p.Observateur, p.Observateur,
p.UsersView,
p.ScoView, p.ScoView,
p.ViewEtudData,
p.UsersView,
), ),
"Secr": ( "Secr": (
p.AbsAddBillet, p.AbsAddBillet,
@ -23,8 +24,9 @@ SCO_ROLES_DEFAULTS = {
p.EtudAddAnnotations, p.EtudAddAnnotations,
p.EtudChangeAdr, p.EtudChangeAdr,
p.Observateur, p.Observateur,
p.UsersView,
p.ScoView, p.ScoView,
p.UsersView,
p.ViewEtudData,
), ),
# Admin est le chef du département, pas le "super admin" # Admin est le chef du département, pas le "super admin"
# on doit donc lister toutes ses permissions: # on doit donc lister toutes ses permissions:
@ -44,9 +46,10 @@ SCO_ROLES_DEFAULTS = {
p.EtudInscrit, p.EtudInscrit,
p.EditFormSemestre, p.EditFormSemestre,
p.Observateur, p.Observateur,
p.ScoView,
p.UsersAdmin, p.UsersAdmin,
p.UsersView, p.UsersView,
p.ScoView, p.ViewEtudData,
), ),
# Rôles pour l'application relations entreprises # Rôles pour l'application relations entreprises
# ObservateurEntreprise est un observateur de l'application entreprise # ObservateurEntreprise est un observateur de l'application entreprise
@ -57,7 +60,8 @@ SCO_ROLES_DEFAULTS = {
p.RelationsEntrepEdit, p.RelationsEntrepEdit,
p.RelationsEntrepViewCorrs, p.RelationsEntrepViewCorrs,
), ),
# AdminEntreprise est un admin de l'application entreprise (toutes les actions possibles de l'application) # AdminEntreprise est un admin de l'application entreprise
# (toutes les actions possibles de l'application)
"AdminEntreprise": ( "AdminEntreprise": (
p.RelationsEntrepView, p.RelationsEntrepView,
p.RelationsEntrepEdit, p.RelationsEntrepEdit,

View File

@ -156,7 +156,7 @@ def trombino_html(groups_infos):
'<a href="%s">%s</a>' '<a href="%s">%s</a>'
% ( % (
url_for( url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"] "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
), ),
foto, foto,
) )

View File

@ -279,7 +279,7 @@ class NonWorkDays(int, BiDirectionalEnum):
] ]
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None: def is_iso_formated(date: str, convert=False) -> bool | datetime.datetime | None:
""" """
Vérifie si une date est au format iso Vérifie si une date est au format iso
@ -298,12 +298,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
return None if convert else False return None if convert else False
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: def localize_datetime(date: datetime.datetime) -> datetime.datetime:
"""Ajoute un timecode UTC à la date donnée. """Transforme une date sans offset en une date avec offset
XXX semble faire autre chose... TODO fix this comment Tente de mettre l'offset de la timezone du serveur (ex : UTC+1)
Si erreur, mettra l'offset UTC
""" """
if isinstance(date, str):
date = is_iso_formated(date, convert=True)
new_date: datetime.datetime = date new_date: datetime.datetime = date
if new_date.tzinfo is None: if new_date.tzinfo is None:
@ -428,7 +427,7 @@ APO_MISSING_CODE_STR = "----" # shown in HTML pages in place of missing code Ap
EDIT_NB_ETAPES = 6 # Nombre max de codes étapes / semestre presentés dans l'UI EDIT_NB_ETAPES = 6 # Nombre max de codes étapes / semestre presentés dans l'UI
IT_SITUATION_MISSING_STR = ( IT_SITUATION_MISSING_STR = (
"____" # shown on ficheEtud (devenir) in place of empty situation "____" # shown on fiche_etud (devenir) in place of empty situation
) )
RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente
@ -476,7 +475,7 @@ MONTH_NAMES_ABBREV = (
"Avr ", "Avr ",
"Mai ", "Mai ",
"Juin", "Juin",
"Jul ", "Juil ",
"Août", "Août",
"Sept", "Sept",
"Oct ", "Oct ",
@ -1282,6 +1281,27 @@ def format_prenom(s):
return " ".join(r) return " ".join(r)
def format_telephone(n: str | None) -> str:
"Format a phone number for display"
if n is None:
return ""
if len(n) < 7:
return n
n = n.replace(" ", "").replace(".", "")
i = 0
r = ""
j = len(n) - 1
while j >= 0:
r = n[j] + r
if i % 2 == 1 and j != 0:
r = " " + r
i += 1
j -= 1
if len(r) == 13 and r[0] != "0":
r = "0" + r
return r
# #
def timedate_human_repr(): def timedate_human_repr():
"representation du temps courant pour utilisateur" "representation du temps courant pour utilisateur"
@ -1610,20 +1630,12 @@ def is_entreprises_enabled():
def is_assiduites_module_forced( def is_assiduites_module_forced(
formsemestre_id: int = None, dept_id: int = None formsemestre_id: int = None, dept_id: int = None
) -> bool: ) -> bool:
"""Vrai si préférence "imposer la saisie du module" sur les assiduités est vraie."""
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
retour: bool return sco_preferences.get_preference(
"forcer_module", formsemestre_id=formsemestre_id, dept_id=dept_id
if dept_id is None: )
dept_id = g.scodoc_dept_id
try:
retour = sco_preferences.get_preference(
"forcer_module", formsemestre_id=int(formsemestre_id)
)
except (TypeError, ValueError):
retour = sco_preferences.get_preference("forcer_module", dept_id=dept_id)
return retour
def get_assiduites_time_config(config_type: str) -> str | int: def get_assiduites_time_config(config_type: str) -> str | int:

View File

@ -256,17 +256,17 @@
background-color: var(--color-conflit); background-color: var(--color-conflit);
} }
.etud_row .assiduites_bar .absent, .etud_row .assiduites_bar>.absent,
.demo.absent { .demo.absent {
background-color: var(--color-absent) !important; background-color: var(--color-absent) !important;
} }
.etud_row .assiduites_bar .present, .etud_row .assiduites_bar>.present,
.demo.present { .demo.present {
background-color: var(--color-present) !important; background-color: var(--color-present) !important;
} }
.etud_row .assiduites_bar .retard, .etud_row .assiduites_bar>.retard,
.demo.retard { .demo.retard {
background-color: var(--color-retard) !important; background-color: var(--color-retard) !important;
} }
@ -275,12 +275,12 @@
background-color: var(--color-nonwork) !important; background-color: var(--color-nonwork) !important;
} }
.etud_row .assiduites_bar .justified, .etud_row .assiduites_bar>.justified,
.demo.justified { .demo.justified {
background-image: var(--motif-justi); background-image: var(--motif-justi);
} }
.etud_row .assiduites_bar .invalid_justified, .etud_row .assiduites_bar>.invalid_justified,
.demo.invalid_justified { .demo.invalid_justified {
background-image: var(--motif-justi-invalide); background-image: var(--motif-justi-invalide);
} }

View File

@ -144,3 +144,7 @@ span.ens-non-reconnu {
.btn:active { .btn:active {
outline: none; outline: none;
} }
.raw-event {
display: none;
}

View File

@ -0,0 +1,212 @@
.day .dayline {
position: absolute;
display: none;
top: 100%;
z-index: 50;
width: max-content;
height: 75px;
background-color: #dedede;
border-radius: 15px;
padding: 5px;
}
.day:hover .dayline {
display: block;
}
.dayline .mini-timeline {
margin-top: 10%;
}
.dayline-title {
margin: 0;
}
.dayline .mini_tick {
position: absolute;
text-align: center;
top: 0;
transform: translateY(-110%);
z-index: 50;
}
.dayline .mini_tick::after {
display: block;
content: "|";
position: absolute;
bottom: -69%;
z-index: 2;
transform: translateX(200%);
}
#label-nom,
#label-justi {
display: none;
}
.demi .day {
display: flex;
justify-content: space-evenly;
}
.demi .day>span {
display: block;
flex: 1;
text-align: center;
z-index: 1;
width: 100%;
border: 1px solid #d5d5d5;
position: relative;
}
.demi .day>span:first-of-type {
width: 3em;
min-width: 3em;
}
.options>* {
margin-right: 5px;
}
.options input {
margin-right: 6px;
}
.options label {
font-weight: normal;
margin-right: 16px;
}
/*Gestion des bubbles*/
.assiduite-bubble {
position: relative;
display: none;
background-color: #f9f9f9;
border-radius: 5px;
padding: 8px;
border: 3px solid #ccc;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
font-size: 12px;
line-height: 1.4;
z-index: 3;
min-width: max-content;
top: 200%;
}
.mini-timeline-block:hover .assiduite-bubble {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: auto;
max-height: 150px;
}
.assiduite-bubble::before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent transparent #f9f9f9 transparent;
}
.assiduite-bubble::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: transparent transparent #ccc transparent;
}
.assiduite-id,
.assiduite-period,
.assiduite-state,
.assiduite-user_id {
margin-bottom: 4px;
}
.assiduite-bubble.absent {
border-color: var(--color-absent) !important;
}
.assiduite-bubble.present {
border-color: var(--color-present) !important;
}
.assiduite-bubble.retard {
border-color: var(--color-retard) !important;
}
/*Gestion des minitimelines*/
.mini-timeline {
height: 7px;
border: 1px solid black;
position: relative;
background-color: white;
}
.mini-timeline.single {
height: 9px;
}
.mini-timeline-block {
position: absolute;
height: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
}
.mini-timeline-block {
cursor: pointer;
}
.mini_tick {
position: absolute;
text-align: start;
top: -40px;
transform: translateX(-50%);
z-index: 1;
}
.mini_tick::after {
display: block;
content: "|";
position: absolute;
bottom: -2px;
z-index: 1;
}
.mini-timeline-block.creneau {
outline: 3px solid var(--color-primary);
pointer-events: none;
}
.mini-timeline-block.absent {
background-color: var(--color-absent) !important;
}
.mini-timeline-block.present {
background-color: var(--color-present) !important;
}
.mini-timeline-block.retard {
background-color: var(--color-retard) !important;
}
.mini-timeline-block.justified {
background-image: var(--motif-justi);
}
.mini-timeline-block.invalid_justified {
background-image: var(--motif-justi-invalide);
}

View File

@ -172,6 +172,11 @@ form#group_selector {
margin-bottom: 3px; margin-bottom: 3px;
} }
/* Text lien ou itms ,non autorisés pour l'utilisateur courant */
.unauthorized {
color: grey;
}
/* ----- bandeau haut ------ */ /* ----- bandeau haut ------ */
span.bandeaugtr { span.bandeaugtr {
width: 100%; width: 100%;
@ -724,7 +729,7 @@ div.scoinfos {
/* ----- fiches etudiants ------ */ /* ----- fiches etudiants ------ */
div.ficheEtud { div.fiche_etud {
background-color: #f5edc8; background-color: #f5edc8;
/* rgb(255,240,128); */ /* rgb(255,240,128); */
border: 1px solid gray; border: 1px solid gray;
@ -739,7 +744,7 @@ div.menus_etud {
margin-top: 1px; margin-top: 1px;
} }
div.ficheEtud h2 { div.fiche_etud h2 {
padding-top: 10px; padding-top: 10px;
} }
@ -925,7 +930,7 @@ td.fichetitre2 {
vertical-align: top; vertical-align: top;
} }
.ficheEtud span.boursier { .fiche_etud span.boursier {
background-color: red; background-color: red;
color: white; color: white;
margin-left: 12px; margin-left: 12px;
@ -963,6 +968,7 @@ div.section_but {
div.section_but > div.link_validation_rcues { div.section_but > div.link_validation_rcues {
align-self: center; align-self: center;
text-align: center;
} }
.ficheannotations { .ficheannotations {
@ -1736,7 +1742,9 @@ formsemestre_page_title .lock img {
width: 200px !important; width: 200px !important;
} }
span.inscr_addremove_menu { div.inscr_addremove_menu {
display: inline-block;
margin: 8px 0px;
width: 150px; width: 150px;
} }

View File

@ -1,3 +1,4 @@
// TODO : Supprimer les fonctions non utilisées + optimiser les fonctions utilisées
// <=== CONSTANTS and GLOBALS ===> // <=== CONSTANTS and GLOBALS ===>
let url; let url;
@ -68,6 +69,25 @@ function setupCheckBox(parent = document) {
}); });
} }
function updateEtudList() {
const group_ids = getGroupIds();
etuds = {};
group_ids.forEach((group_id) => {
sync_get(getUrl() + `/api/group/${group_id}/etudiants`, (data, status) => {
if (status === "success") {
data.forEach((etud) => {
if (!(etud.id in etuds)) {
etuds[etud.id] = etud;
}
});
}
});
});
getAssiduitesFromEtuds(true);
generateAllEtudRow();
}
/** /**
* Validation préalable puis désactivation des chammps : * Validation préalable puis désactivation des chammps :
* - Groupe * - Groupe
@ -108,14 +128,16 @@ function validateSelectors(btn) {
return; return;
} }
getAssiduitesFromEtuds(true);
// document.querySelector(".selectors").disabled = true;
// $("#tl_date").datepicker("option", "disabled", true);
generateMassAssiduites(); generateMassAssiduites();
getAssiduitesFromEtuds(true);
generateAllEtudRow(); generateAllEtudRow();
// btn.remove();
btn.textContent = "Actualiser"; btn.remove();
// Auto actualisation
$("#tl_date").on("change", updateEtudList);
$("#group_ids_sel").on("change", updateEtudList);
onlyAbs(); onlyAbs();
}; };
@ -648,16 +670,15 @@ function updateDate() {
); );
openAlertModal("Attention", div, "", "#eec660"); openAlertModal("Attention", div, "", "#eec660");
/* BUG TODO MATHIAS
$(dateInput).datepicker("setDate", date_fra); // XXX ??? non définie
dateInput.value = date_fra;
*/
date = lastWorkDay; date = lastWorkDay;
dateStr = formatDate(lastWorkDay, { dateStr = formatDate(lastWorkDay, {
dateStyle: "full", dateStyle: "full",
timeZone: SCO_TIMEZONE, timeZone: SCO_TIMEZONE,
}).capitalize(); }).capitalize();
$(dateInput).datepicker("setDate", date);
$(dateInput).change();
} }
document.querySelector("#datestr").textContent = dateStr; document.querySelector("#datestr").textContent = dateStr;
@ -697,6 +718,61 @@ function setupDate(onchange = null) {
} }
}); });
//Initialisation du datepicker
// sinon on ne peut pas le mettre à jour
// XXX TODO-assiduite : finir tester + éviter duplication code avec scodoc.js
$(input).datepicker({
showOn: "button",
buttonImage: "/ScoDoc/static/icons/calendar_img.png",
buttonImageOnly: true,
dateFormat: "dd/mm/yy",
duration: "fast",
firstDay: 1, // Start with Monday
dayNames: [
"Dimanche",
"Lundi",
"Mardi",
"Mercredi",
"Jeudi",
"Vendredi",
"Samedi",
],
dayNamesMin: ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"],
dayNamesShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"],
monthNames: [
"Janvier",
"Février",
"Mars",
"Avril",
"May",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Décembre",
],
monthNamesShort: [
"Jan",
"Fév",
"Mar",
"Avr",
"Mai",
"Juin",
"Juil",
"Aoû",
"Sep",
"Oct",
"Nov",
"Déc",
],
});
$(input).datepicker(
"option",
$.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"])
);
if (onchange != null) { if (onchange != null) {
$(input).change(onchange); $(input).change(onchange);
} }
@ -1262,19 +1338,14 @@ function getAllAssiduitesFromEtud(
.replace("°", courant ? "&courant" : "") .replace("°", courant ? "&courant" : "")
: "" : ""
}`; }`;
//TODO Utiliser async_get au lieu de jquery async_get(
$.ajax({ url_api,
async: true, (data) => {
type: "GET", assiduites[etudid] = data;
url: url_api, action(data);
success: (data, status) => {
if (status === "success") {
assiduites[etudid] = data;
action(data);
}
}, },
error: () => {}, (_) => {}
}); );
} }
/** /**
@ -1844,18 +1915,13 @@ function getAllJustificatifsFromEtud(
order ? "/query?order°".replace("°", courant ? "&courant" : "") : "" order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
}`; }`;
//TODO Utiliser async_get au lieu de jquery async_get(
$.ajax({ url_api,
async: true, (data) => {
type: "GET", action(data);
url: url_api,
success: (data, status) => {
if (status === "success") {
action(data);
}
}, },
error: () => {}, () => {}
}); );
} }
function deleteJustificatif(justif_id) { function deleteJustificatif(justif_id) {

View File

@ -39,7 +39,7 @@ $(function () {
"Avril", "Avril",
"May", "May",
"Juin", "Juin",
"Juilet", "Juillet",
"Août", "Août",
"Septembre", "Septembre",
"Octobre", "Octobre",

View File

@ -1,14 +1,76 @@
from datetime import datetime from datetime import datetime
from flask import url_for from flask import url_for
from flask_sqlalchemy.query import Pagination, Query from flask_login import current_user
from sqlalchemy import desc, literal, union from flask_sqlalchemy.query import Query
from sqlalchemy import desc, literal, union, asc
from app import db, g from app import db, g
from app.auth.models import User from app.auth.models import User
from app.models import Assiduite, Identite, Justificatif from app.models import Assiduite, Identite, Justificatif
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
from app.tables import table_builder as tb from app.tables import table_builder as tb
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
from app.scodoc.sco_permissions import Permission
class Pagination:
"""
Pagination d'une collection de données
On donne :
- une collection de données (de préférence une liste / tuple)
- le numéro de page à afficher
- le nombre d'éléments par page
On peut ensuite récupérer les éléments de la page courante avec la méthode `items()`
Cette classe ne permet pas de changer de page.
(Pour cela, il faut créer une nouvelle instance, avec la collection originelle et la nouvelle page)
l'intéret est de ne pas garder en mémoire toute la collection, mais seulement la page courante
"""
def __init__(self, collection: list, page: int = 1, per_page: int = -1):
"""
__init__ Instancie un nouvel objet Pagination
Args:
collection (list): La collection à paginer. Il s'agit par exemple d'une requête
page (int, optional): le numéro de la page à voir. Defaults to 1.
per_page (int, optional): le nombre d'éléments par page. Defaults to -1. (-1 = pas de pagination/tout afficher)
"""
# par défaut le total des pages est 1 (même si la collection est vide)
self.total_pages = 1
if per_page != -1:
# on récupère le nombre de page complète et le reste
# q => nombre de page
# r => le nombre d'éléments restants (dernière page si != 0)
q, r = len(collection) // per_page, len(collection) % per_page
self.total_pages = q if r == 0 else q + 1 # q + 1 s'il reste des éléments
# On s'assure que la page demandée est dans les limites
current_page: int = min(self.total_pages, page if page > 0 else 1)
# On récupère la collection de la page courante
self.collection = (
collection # toute la collection si pas de pagination
if per_page == -1
else collection[
per_page * (current_page - 1) : per_page * (current_page)
] # sinon on récupère la page
)
def items(self) -> list:
"""
items Renvoi la collection de la page courante
Returns:
list: la collection de la page courante
"""
return self.collection
class ListeAssiJusti(tb.Table): class ListeAssiJusti(tb.Table):
@ -18,13 +80,15 @@ class ListeAssiJusti(tb.Table):
""" """
NB_PAR_PAGE: int = 25 NB_PAR_PAGE: int = 25
MAX_PAR_PAGE: int = 200 MAX_PAR_PAGE: int = 1000
def __init__( def __init__(
self, self,
table_data: "AssiJustifData", table_data: "AssiJustifData",
filtre: "AssiFiltre" = None, filtre: "AssiFiltre" = None,
options: "AssiDisplayOptions" = None, options: "AssiDisplayOptions" = None,
no_pagination: bool = False,
titre: str = "",
**kwargs, **kwargs,
) -> None: ) -> None:
""" """
@ -41,11 +105,21 @@ class ListeAssiJusti(tb.Table):
# Gestion des options, par défaut un objet Options vide # Gestion des options, par défaut un objet Options vide
self.options = options if options is not None else AssiDisplayOptions() self.options = options if options is not None else AssiDisplayOptions()
self.no_pagination: bool = no_pagination
self.total_page: int = None self.total_page: int = None
# Accès aux détail des justificatifs ?
self.can_view_justif_detail = current_user.has_permission(
Permission.AbsJustifView
)
# les lignes du tableau # les lignes du tableau
self.rows: list["RowAssiJusti"] = [] self.rows: list["RowAssiJusti"] = []
# Titre du tableau, utilisé pour le cache
self.titre = titre
# Instanciation de la classe parent # Instanciation de la classe parent
super().__init__( super().__init__(
row_class=RowAssiJusti, row_class=RowAssiJusti,
@ -65,59 +139,93 @@ class ListeAssiJusti(tb.Table):
# Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi # Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi
type_obj = self.filtre.type_obj() type_obj = self.filtre.type_obj()
if type_obj in [0, 1]: cle_cache: str = ":".join(
assiduites_query_etudiants = self.table_data.assiduites_query map(
str,
# Non affichage des présences [
if not self.options.show_pres: self.titre,
assiduites_query_etudiants = assiduites_query_etudiants.filter( type_obj,
Assiduite.etat != EtatAssiduite.PRESENT self.options.show_pres,
) self.options.show_reta,
# Non affichage des retards self.options.show_desc,
if not self.options.show_reta: self.options.order[0],
assiduites_query_etudiants = assiduites_query_etudiants.filter( self.options.order[1],
Assiduite.etat != EtatAssiduite.RETARD ],
) )
if type_obj in [0, 2]:
justificatifs_query_etudiants = self.table_data.justificatifs_query
# Combinaison des requêtes
query_finale: Query = self.joindre(
query_assiduite=assiduites_query_etudiants,
query_justificatif=justificatifs_query_etudiants,
) )
r = RequeteTableauAssiduiteCache().get(cle_cache)
if r is None:
if type_obj in [0, 1]:
assiduites_query_etudiants = self.table_data.assiduites_query
# Non affichage des présences
if (
not self.options.show_pres
and assiduites_query_etudiants is not None
):
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.PRESENT
)
# Non affichage des retards
if (
not self.options.show_reta
and assiduites_query_etudiants is not None
):
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.RETARD
)
if type_obj in [0, 2]:
justificatifs_query_etudiants = self.table_data.justificatifs_query
# Combinaison des requêtes
query_finale: Query = self.joindre(
query_assiduite=assiduites_query_etudiants,
query_justificatif=justificatifs_query_etudiants,
)
# Tri de la query si option
if self.options.order is not None:
order_sort: str = asc if self.options.order[1] else desc
order_col: str = self.options.order[0]
query_finale: Query = query_finale.order_by(order_sort(order_col))
r = query_finale.all()
RequeteTableauAssiduiteCache.set(cle_cache, r)
# Paginer la requête pour ne pas envoyer trop d'informations au client # Paginer la requête pour ne pas envoyer trop d'informations au client
pagination: Pagination = self.paginer(query_finale) pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
self.total_pages: int = pagination.pages self.total_pages = pagination.total_pages
# Générer les lignes de la page # Générer les lignes de la page
for ligne in pagination.items: for ligne in pagination.items():
row: RowAssiJusti = self.row_class(self, ligne._asdict()) row: RowAssiJusti = self.row_class(self, ligne._asdict())
row.ajouter_colonnes() row.ajouter_colonnes()
self.add_row(row) self.add_row(row)
def paginer(self, query: Query) -> Pagination: def paginer(self, collection: list, no_pagination: bool = False) -> Pagination:
""" """
Applique la pagination à une requête SQLAlchemy en fonction des paramètres de la classe. Applique une pagination à une collection en fonction des paramètres de la classe.
Cette méthode prend une requête SQLAlchemy et applique la pagination en utilisant les Cette méthode prend une collection et applique la pagination en utilisant les
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`. attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
Args: Args:
query (Query): La requête SQLAlchemy à paginer. Il s'agit d'une requête qui a déjà collection (list): La collection à paginer. Il s'agit par exemple d'une requête qui a déjà
été construite et qui est prête à être exécutée. été construite et qui est prête à être exécutée.
Returns: Returns:
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée. Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
Note: Note:
Cette méthode ne modifie pas la requête originale; elle renvoie plutôt un nouvel Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
objet qui contient les résultats paginés. objet qui contient les résultats paginés.
""" """
return query.paginate( return Pagination(
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False collection,
self.options.page,
-1 if no_pagination else self.options.nb_ligne_page,
) )
def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None): def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None):
@ -172,7 +280,7 @@ class ListeAssiJusti(tb.Table):
] ]
if self.options.show_desc: if self.options.show_desc:
assiduites_entities.append(Assiduite.description.label("description")) assiduites_entities.append(Assiduite.description.label("desc"))
query_assiduite = query_assiduite.with_entities(*assiduites_entities) query_assiduite = query_assiduite.with_entities(*assiduites_entities)
queries.append(query_assiduite) queries.append(query_assiduite)
@ -194,7 +302,7 @@ class ListeAssiJusti(tb.Table):
] ]
if self.options.show_desc: if self.options.show_desc:
justificatifs_entities.append(Justificatif.raison.label("description")) justificatifs_entities.append(Justificatif.raison.label("desc"))
query_justificatif = query_justificatif.with_entities( query_justificatif = query_justificatif.with_entities(
*justificatifs_entities *justificatifs_entities
@ -210,7 +318,7 @@ class ListeAssiJusti(tb.Table):
# Combiner les requêtes avec une union # Combiner les requêtes avec une union
query_combinee = union(*queries).alias("combinee") query_combinee = union(*queries).alias("combinee")
query_combinee = db.session.query(query_combinee).order_by(desc("date_debut")) query_combinee = db.session.query(query_combinee)
return query_combinee return query_combinee
@ -241,30 +349,46 @@ class RowAssiJusti(tb.Row):
# Type d'objet # Type d'objet
self._type() self._type()
# Date de début # En excel, on export les "vraies dates".
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
# En excel, on export les "vraes dates".
# En HTML, on écrit en français (on laisse les dates pour le tri) # En HTML, on écrit en français (on laisse les dates pour le tri)
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
date_affichees: list[str] = [
self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"), # date début
self.ligne["date_fin"].strftime("%d/%m/%y de %H:%M"), # date fin
]
if multi_days:
date_affichees[0] = self.ligne["date_debut"].strftime("%d/%m/%y")
date_affichees[1] = self.ligne["date_fin"].strftime("%d/%m/%y")
self.add_cell( self.add_cell(
"date_debut", "date_debut",
"Date de début", "Date de début",
self.ligne["date_debut"].strftime("%d/%m/%y") date_affichees[0],
if multi_days
else self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"),
data={"order": self.ligne["date_debut"]}, data={"order": self.ligne["date_debut"]},
raw_content=self.ligne["date_debut"], raw_content=self.ligne["date_debut"],
column_classes={"date", "date-debut"}, column_classes={
"date",
"date-debut",
"external-sort",
"external-type:date_debut",
},
) )
# Date de fin # Date de fin
self.add_cell( self.add_cell(
"date_fin", "date_fin",
"Date de fin", "Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y") date_affichees[1],
if multi_days
else self.ligne["date_fin"].strftime("à %H:%M"),
raw_content=self.ligne["date_fin"], # Pour excel raw_content=self.ligne["date_fin"], # Pour excel
data={"order": self.ligne["date_fin"]}, data={"order": self.ligne["date_fin"]},
column_classes={"date", "date-fin"}, column_classes={
"date",
"date-fin",
"external-sort",
"external-type:date_fin",
},
) )
# Ajout des colonnes optionnelles # Ajout des colonnes optionnelles
@ -283,7 +407,11 @@ class RowAssiJusti(tb.Row):
data={"order": self.ligne["entry_date"] or ""}, data={"order": self.ligne["entry_date"] or ""},
raw_content=self.ligne["entry_date"], raw_content=self.ligne["entry_date"],
classes=["small-font"], classes=["small-font"],
column_classes={"entry_date"}, column_classes={
"entry_date",
"external-sort",
"external-type:entry_date",
},
) )
def _type(self) -> None: def _type(self) -> None:
@ -349,10 +477,21 @@ class RowAssiJusti(tb.Row):
def _optionnelles(self) -> None: def _optionnelles(self) -> None:
if self.table.options.show_desc: if self.table.options.show_desc:
if self.ligne.get("type") == "justificatif":
# protection de la "raison"
if (
self.ligne["user_id"] == current_user.id
or self.table.can_view_justif_detail
):
description = self.ligne["desc"] if self.ligne["desc"] else ""
else:
description = "(cachée)"
else:
description = self.ligne["desc"] if self.ligne["desc"] else ""
self.add_cell( self.add_cell(
"description", "description",
"Description", "Description",
self.ligne["description"] if self.ligne["description"] else "", description,
) )
if self.table.options.show_module: if self.table.options.show_module:
if self.ligne["type"] == "assiduite": if self.ligne["type"] == "assiduite":
@ -415,9 +554,13 @@ class RowAssiJusti(tb.Row):
) )
html.append(f'<a title="Supprimer" href="{url}">❌</a>') # utiliser url_for html.append(f'<a title="Supprimer" href="{url}">❌</a>') # utiliser url_for
# Justifier (si type Assiduité et est_just faux) # Justifier (si type Assiduité, etat != Présent et est_just faux)
if self.ligne["type"] == "assiduite" and not self.ligne["est_just"]: if (
self.ligne["type"] == "assiduite"
and self.ligne["etat"] != EtatAssiduite.PRESENT
and not self.ligne["est_just"]
):
url = url_for( url = url_for(
"assiduites.tableau_assiduite_actions", "assiduites.tableau_assiduite_actions",
type=self.ligne["type"], type=self.ligne["type"],
@ -541,6 +684,7 @@ class AssiDisplayOptions:
show_etu: str | bool = True, show_etu: str | bool = True,
show_actions: str | bool = True, show_actions: str | bool = True,
show_module: str | bool = False, show_module: str | bool = False,
order: tuple[str, str | bool] = None,
): ):
self.page: int = page self.page: int = page
self.nb_ligne_page: int = nb_ligne_page self.nb_ligne_page: int = nb_ligne_page
@ -554,6 +698,10 @@ class AssiDisplayOptions:
self.show_actions = to_bool(show_actions) self.show_actions = to_bool(show_actions)
self.show_module = to_bool(show_module) self.show_module = to_bool(show_module)
self.order = (
("date_debut", False) if order is None else (order[0], to_bool(order[1]))
)
def remplacer(self, **kwargs): def remplacer(self, **kwargs):
"Positionne options booléennes selon arguments" "Positionne options booléennes selon arguments"
for k, v in kwargs.items(): for k, v in kwargs.items():
@ -565,6 +713,12 @@ class AssiDisplayOptions:
self.nb_ligne_page = min( self.nb_ligne_page = min(
self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE
) )
elif k == "order":
setattr(
self,
k,
("date_debut", False) if v is None else (v[0], to_bool(v[1])),
)
class AssiJustifData: class AssiJustifData:

View File

@ -129,41 +129,44 @@ class RowAssi(tb.Row):
) )
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]: def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
# XXX TODO @iziram commentaire sur la fonction et la var. retour """
Renvoie le comptage (dans la métrique du département) des différents états d'assiduité d'un étudiant
Returns :
{
"<etat>" : [<Etat version lisible>, <nb total etat>, <nb just etat>]
}
"""
# Préparation du retour
retour: dict[str, tuple[str, float, float]] = { retour: dict[str, tuple[str, float, float]] = {
"absent": ["Absences", 0.0, 0.0], "absent": ["Absences", 0.0, 0.0],
"retard": ["Retards", 0.0, 0.0], "retard": ["Retards", 0.0, 0.0],
"present": ["Présences", 0.0, 0.0], "present": ["Présences", 0.0, 0.0],
} }
# Récupération de la métrique du département
assi_metric = scu.translate_assiduites_metric( assi_metric = scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id), sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
) )
compte_etat: dict[str, dict] = scass.get_assiduites_stats(
assiduites=etud.assiduites,
metric=assi_metric,
filtered={
"date_debut": self.dates[0],
"date_fin": self.dates[1],
"etat": "absent,present,retard", # pour tout compter d'un coup
"split": 1, # afin d'avoir la division des stats en état, etatjust, etatnonjust
},
)
# Pour chaque état on met à jour les valeurs de retour
for etat, valeur in retour.items(): for etat, valeur in retour.items():
compte_etat = scass.get_assiduites_stats( valeur[1] = compte_etat[etat][assi_metric]
assiduites=etud.assiduites, if etat != "present":
metric=assi_metric, valeur[2] = compte_etat[etat]["justifie"][assi_metric]
filtered={
"date_debut": self.dates[0],
"date_fin": self.dates[1],
"etat": etat,
},
)
compte_etat_just = scass.get_assiduites_stats(
assiduites=etud.assiduites,
metric=assi_metric,
filtered={
"date_debut": self.dates[0],
"date_fin": self.dates[1],
"etat": etat,
"est_just": True,
},
)
valeur[1] = compte_etat[assi_metric]
valeur[2] = compte_etat_just[assi_metric]
return retour return retour

View File

@ -29,6 +29,15 @@
</em> </em>
</p> </p>
<div>
<h2>Coordonnées du délégué à la protection des données (DPO)</h2>
{% if ScoDocSiteConfig.get("rgpd_coordonnees_dpo") %}
{{ ScoDocSiteConfig.get("rgpd_coordonnees_dpo") }}
{% else %}
<em>non renseigné</em>
{% endif %}
</div>
<h2>Dernières évolutions</h2> <h2>Dernières évolutions</h2>
{{ news|safe }} {{ news|safe }}

View File

@ -6,7 +6,6 @@
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock %} {% endblock %}
@ -88,6 +87,13 @@ div.submit > input {
{{ form.modimpl }} {{ form.modimpl }}
{{ render_field_errors(form, 'modimpl') }} {{ render_field_errors(form, 'modimpl') }}
</div> </div>
{# Justifiée #}
<div class="est-justifiee">
{{ form.est_just.label }}&nbsp;:
{{ form.est_just }}
<span class="help">génère un justificatif valide ayant la même période que l'assiduité signalée</span>
{{ render_field_errors(form, 'est_just') }}
</div>
{# Description #} {# Description #}
<div> <div>
<div>{{ form.description.label }}</div> <div>{{ form.description.label }}</div>
@ -114,19 +120,7 @@ div.submit > input {
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script> <script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script> <script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script> {% include "sco_timepicker.j2" %}
$('.timepicker').timepicker({
timeFormat: 'HH:mm',
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
dynamic: false,
dropdown: true,
scrollbar: false
});
</script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -1,246 +0,0 @@
{% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% block pageContent %}
<div class="pageContent">
<h3>Signaler une absence, présence ou retard pour {{etud.html_link_fiche()|safe}}</h3>
{% if saisie_eval %}
<div id="saisie_eval">
<br>
<h3>
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>
{% endif %}
<section class="assi-form page">
<fieldset>
<div class="assi-row">
<div class="assi-label">
<legend for="assi_date_debut" required>Date de début</legend>
<input type="text" name="assi_date_debut" id="assi_date_debut" size="10"
class="datepicker">
<input type="text" name="assi_heure_debut" id="assi_heure_debut" size="5"
class="timepicker">
<span>Journée entière</span> <input type="checkbox" name="assi_journee" id="assi_journee">
</div>
<div class="assi-label" id="date_fin">
<legend for="assi_date_fin" required>Date de fin</legend>
<scodoc-datetime name="assi_date_fin" id="assi_date_fin"></scodoc-datetime>
</div>
</div>
<div class="assi-row">
<div class="assi-label">
<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>
<option value="present">Présent</option>
</select>
</div>
</div>
<div class="assi-row">
<div class="assi-label">
<legend for="assi_module" required>Module</legend>
{% with moduleid="ajout_assiduite_module_impl",label=false %}
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
{% endwith %}
</div>
</div>
<div class="assi-row">
<div class="assi-label">
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="75" rows="4" maxlength="500"></textarea>
</div>
</div>
<div class="assi-row">
<button onclick="validerFormulaire(this)">Enregistrer</button>
<button onclick="effacerFormulaire()">Remettre à zero</button>
</div>
</fieldset>
</section>
<section class="assi-liste">
{{tableau | safe }}
</section>
</div>
<style>
.assi-row {
margin: 5px 0;
}
.assi-form fieldset {
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.pageContent {
max-width: var(--sco-content-max-width);
margin-top: 15px;
}
.assi-label {
margin: 0 10px;
}
[required]::after {
content: "*";
color: var(--color-error);
}
</style>
<script>
$('.timepicker').timepicker({
timeFormat: 'HH:mm',
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
defaultTime: 'now',
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
dynamic: false,
dropdown: true,
scrollbar: false
});
function validateFields() {
const field = document.querySelector('.assi-form')
const { deb, fin } = getDates()
const date_debut = new Date(deb);
const date_fin = new Date(fin);
if (deb == "" || fin == "" || !date_debut.isValid() || !date_fin.isValid()) {
openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson");
return false;
}
if (date_fin.isBefore(date_debut)) {
openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson");
return false;
}
return true
}
function fieldsToAssiduite() {
const field = document.querySelector('.assi-form.page')
const { deb, fin } = getDates()
const etat = field.querySelector('#assi_etat').value;
const raison = field.querySelector('#raison').value;
const module = field.querySelector("#ajout_assiduite_module_impl").value;
return {
date_debut: new Date(deb).toFakeIso(),
date_fin: new Date(fin).toFakeIso(),
etat: etat,
description: raison,
moduleimpl_id: module,
}
}
function validerFormulaire(btn) {
if (!validateFields()) return
const assiduite = fieldsToAssiduite();
let assiduite_id = null;
createAssiduiteComplete(assiduite, etudid);
updateTableau();
btn.disabled = true;
setTimeout(() => {
btn.disabled = false;
}, 1000)
}
function effacerFormulaire() {
const field = document.querySelector('.assi-form')
field.querySelector('#assi_date_debut').value = "";
field.querySelector('#assi_date_fin').value = "";
field.querySelector('#assi_etat').value = "attente";
field.querySelector('#raison').value = "";
}
function dayOnly() {
const date_deb = document.getElementById("assi_date_debut");
const date_fin = document.getElementById("assi_date_fin");
if (document.getElementById('assi_journee').checked) {
date_deb.setAttribute("show", "date")
date_fin.setAttribute("show", "date")
document.querySelector(`legend[for="assi_date_fin"]`).removeAttribute("required")
} else {
date_deb.removeAttribute("show")
date_fin.removeAttribute("show")
document.querySelector(`legend[for="assi_date_fin"]`).setAttribute("required", "")
}
}
function getDates() {
const date_deb = document.querySelector(".page #assi_date_debut")
const date_fin = document.querySelector(".page #assi_date_fin")
const journee = document.querySelector('.page #assi_journee').checked
const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time)
let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
if (journee) {
fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin
} else {
fin = date_fin.valueAsObject.date + fin
}
return {
"deb": deb,
"fin": fin,
}
}
const etudid = {{ sco.etud.id }};
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
const assi_morning = '{{assi_morning}}';
const assi_evening = '{{assi_evening}}';
{% if saisie_eval %}
const saisie_eval = true;
const date_deb = "{{date_deb}}";
const date_fin = "{{date_fin}}";
const moduleimpl = {{ moduleimpl_id }};
{% else %}
const saisie_eval = false;
{% endif %}
window.addEventListener("load", () => {
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
dayOnly()
if (saisie_eval) {
document.getElementById("assi_date_debut").value = Date.removeUTC(date_deb);
document.getElementById("assi_date_fin").value = Date.removeUTC(date_fin);
} else {
const today = (new Date()).format("YYYY-MM-DD");
document.getElementById("assi_date_debut").valueAsObject = { date: today, time: assi_morning }
document.getElementById("assi_date_fin").valueAsObject = { time: assi_evening }
}
document.getElementById("assi_date_debut").addEventListener("blur", (event) => {
updateSelect(null, "#ajout_assiduite_module_impl", event.target.valueAsObject.date)
})
updateSelect(saisie_eval ? moduleimpl : "", "#ajout_assiduite_module_impl", document.getElementById("assi_date_debut").valueAsObject.date);
});
</script>
{% endblock pageContent %}

View File

@ -5,7 +5,6 @@ Si justif, edit #}
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock %} {% endblock %}
@ -18,6 +17,9 @@ form#ajout-justificatif-etud {
form#ajout-justificatif-etud > div { form#ajout-justificatif-etud > div {
margin-bottom: 16px; margin-bottom: 16px;
} }
fieldset > div {
margin-bottom: 12px;
}
div.fichiers { div.fichiers {
margin-top: 16px; margin-top: 16px;
margin-bottom: 32px; margin-bottom: 32px;
@ -34,9 +36,21 @@ div.submit {
div.submit > input { div.submit > input {
margin-right: 16px; margin-right: 16px;
} }
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style> </style>
<div class="tab-content"> <div class="tab-content">
<h2>Justifier des absences ou retards</h2> <h2>{{title|safe}}</h2>
{% if justif %}
<div class="info-saisie">
Saisie par {{justif.user.get_prenomnom() if justif.user else "inconnu"}}
le {{justif.entry_date.strftime("%d/%m/%Y à %H:%M") if justif.entry_date else "?"}}
</div>
{% endif %}
<section class="justi-form page"> <section class="justi-form page">
@ -73,16 +87,24 @@ div.submit > input {
</div> </div>
{# Raison #} {# Raison #}
<div> <div>
<div>{{ form.raison.label }}</div> {% if (not justif) or can_view_justif_detail %}
{{ form.raison() }} <div>{{ form.raison.label }}</div>
{{ render_field_errors(form, 'raison') }} {{ form.raison() }}
{{ render_field_errors(form, 'raison') }}
<div class="help">La raison sera visible aux utilisateurs ayant le droit
<tt>AbsJustifView</tt> et à celui ayant déposé le justificatif
{%- if justif %} (<b>{{justif.user.get_prenomnom()}}</b>){%- endif -%}.
</div>
{% else %}
<div class="unauthorized">raison confidentielle</div>
{% endif %}
</div> </div>
<div class="fichiers"> <div class="fichiers">
{# Liste des fichiers existants #} {# Liste des fichiers existants #}
{% if justif and nb_files > 0 %} {% if justif and nb_files > 0 %}
<div><b>{{nb_files}} fichiers justificatifs déposés <div><b>{{nb_files}} fichiers justificatifs déposés
{% if filenames|length < nb_files %} {% if filenames|length < nb_files %}
, dont {{filenames|length}} vous sont accessibles , dont {{filenames|length}} vous {{'sont accessibles' if filenames|length > 1 else 'est accessible'}}
{% endif %} {% endif %}
</b> </b>
</div> </div>
@ -105,6 +127,7 @@ div.submit > input {
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} {{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}
<span class="help">laisser vide pour date courante</span> <span class="help">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }} {{ render_field_errors(form, 'entry_date') }}
{# Submit #} {# Submit #}
<div class="submit"> <div class="submit">
{{ form.submit }} {{ form.cancel }} {{ form.submit }} {{ form.cancel }}
@ -126,21 +149,9 @@ div.submit > input {
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script> <script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script> <script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script> {% include "sco_timepicker.j2" %}
$('.timepicker').timepicker({
timeFormat: 'HH:mm',
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
dynamic: false,
dropdown: true,
scrollbar: false
});
</script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Suppression d'un fichier justificatif // Suppression d'un fichier justificatif

View File

@ -1,187 +1,27 @@
{% include "assiduites/widgets/tableau_base.j2" %} {% extends "sco_page.j2" %}
<section class="alerte invisible"> {% block styles %}
<p>Attention, cet étudiant a trop d'absences</p> {{super()}}
</section> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock scripts %}
{% block app_content %}
<h2>Traitement de l'assiduité</h2>
<p class="help">
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
le semestre concerné (saisie par jour ou saisie différée).
</p>
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
choisissez d'abord la personne concernée&nbsp;:</p>
<br>
{{search_etud | safe}}
<br>
{{billets | safe}}
<br>
<section class="nonvalide"> <section class="nonvalide">
<!-- Tableaux des justificatifs à valider (attente / modifié ) --> {{tableau | safe }}
<h4>Justificatifs en attente (ou modifiés)</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti(true)"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
{% endblock app_content %}
<div class="annee">
<span>Année scolaire 2022-2023 Changer année: </span>
<select name="" id="annee" onchange="setterAnnee(this.value)">
</select>
</div>
<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>
</div>
<script>
let formsemestre_id = "{{formsemestre_id}}"
let group_id = "{{group_id}}"
function getDeptJustificatifsFromPeriod(action) {
const formsemestre = formsemestre_id ? `&formsemestre_id=${formsemestre_id}` : ""
const group = group_id ? `&group_id=${group_id}` : ""
const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}${formsemestre}${group}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
justificatifCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function generate(annee) {
if (annee < 1999 || annee > 2999) {
openAlertModal("Année impossible", document.createTextNode("L'année demandé n'existe pas."));
return;
}
bornes = {
deb: `${annee}-09-01T00:00`,
fin: `${annee + 1}-08-31T23:59`
}
defAnnee = annee;
loadAll();
}
function getJusti(action) {
try { getDeptJustificatifsFromPeriod(action) } catch (_) { }
}
function setterAnnee(annee) {
annee = parseInt(annee);
document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: `
generate(annee)
}
let defAnnee = {{ annee }};
let bornes = {
deb: `${defAnnee}-09-01T00:00`,
fin: `${defAnnee + 1}-08-31T23:59`
}
const dept_id = {{ dept_id }};
let annees = {{ annees | safe}}
annees = annees.filter((x, i) => annees.indexOf(x) === i)
window.addEventListener('load', () => {
filterJustificatifs = {
"columns": [
"formsemestre",
"etudid",
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
"etat": [
"attente",
"modifie"
],
}
}
const select = document.querySelector('#annee');
annees.forEach((a) => {
const opt = document.createElement("option");
opt.value = a + "",
opt.textContent = `${a} - ${a + 1}`;
if (a === defAnnee) {
opt.selected = true;
}
select.appendChild(opt)
})
setterAnnee(defAnnee)
})
</script>
<style>
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
.stats {
border: 1px solid #333;
padding: 5px 2px;
width: fit-content;
}
.stats-values {
display: flex;
justify-content: flex-start;
gap: 15px;
}
.stats-values-item h5 {
font-weight: bold;
text-decoration-line: underline;
}
.stats-values-part {
display: flex;
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
border-radius: 7px;
background-color: var(--color-error);
}
.alerte.invisible {
display: none;
}
.alerte p {
font-size: larger;
color: whitesmoke;
}
.suppr {
margin: 5px 0;
}
</style>

View File

@ -1,3 +1,70 @@
{% extends "sco_page.j2" %}
{% block title %}
Bilan assiduité de {{sco.etud.nomprenom}}
{% endblock title %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<style>
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
.stats {
border: 1px solid #333;
padding: 5px 2px;
width: fit-content;
}
.stats-values {
display: flex;
justify-content: flex-start;
gap: 15px;
}
.stats-values-item h5 {
font-weight: bold;
text-decoration-line: underline;
}
.stats-values-part {
display: flex;
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
border-radius: 7px;
background-color: var(--color-error);
}
.alerte.invisible {
display: none;
}
.alerte p {
font-size: larger;
color: whitesmoke;
}
.suppr {
margin: 5px 0;
}
</style>
{% endblock styles %}
{% block app_content %} {% block app_content %}
{% include "assiduites/widgets/tableau_base.j2" %} {% include "assiduites/widgets/tableau_base.j2" %}
<div class="pageContent"> <div class="pageContent">
@ -12,9 +79,9 @@
<!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié --> <!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié -->
<h4>Statistiques d'assiduité</h4> <h4>Statistiques d'assiduité</h4>
<div class="stats-inputs"> <div class="stats-inputs">
<label class="stats-label"> Date de début<input type="text" class="datepicker" name="stats_date_debut" <label class="stats-label"> Date de début <input type="text" class="datepicker" name="stats_date_debut"
id="stats_date_debut" value="{{date_debut}}"></label> id="stats_date_debut" value="{{date_debut}}"></label>
<label class="stats-label"> Date de fin<input type="text" class="datepicker" name="stats_date_fin" <label class="stats-label"> Date de fin <input type="text" class="datepicker" name="stats_date_fin"
id="stats_date_fin" value="{{date_fin}}"></label> id="stats_date_fin" value="{{date_fin}}"></label>
<button onclick="stats()">Actualiser</button> <button onclick="stats()">Actualiser</button>
</div> </div>
@ -25,27 +92,7 @@
</section> </section>
<section class="nonvalide"> <section class="nonvalide">
<!-- Tableaux des assiduités (retard/abs) non justifiées --> {{tableau | safe }}
<h4>Absences et retards non justifiés</h4>
{# XXX XXX XXX #}
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
</div>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<h4>Justificatifs en attente (ou modifiés)</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
<section class="suppr"> <section class="suppr">
@ -60,36 +107,18 @@
département)</p> département)</p>
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates, <p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
appuyer sur le bouton "Actualiser"</p> appuyer sur le bouton "Actualiser"</p>
<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>Éditer : modifie le justificatif (dates, état, ajouter/supprimer fichier, etc.)</li>
<li>Supprimer : supprime le justificatif (action irréversible)</li>
</ul>
<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'élément sélectionnée</li>
<li>Editer : modifie l'élément (module, état)</li>
<li>Supprimer : supprime l'élément (action irréversible)</li>
</ul>
</div> </div>
</div> </div>
{% endblock app_content %} {% endblock app_content %}
<script>
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
<script>
function stats() { function stats() {
const dd_val = document.getElementById('stats_date_debut').value; const dd_val = document.getElementById('stats_date_debut').value;
const df_val = document.getElementById('stats_date_fin').value; const df_val = document.getElementById('stats_date_fin').value;
@ -111,89 +140,76 @@
} }
function getAssiduitesCount(dateDeb, dateFin, query) { function getAssiduitesCount(dateDeb, dateFin, action) {
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`; const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
//Utiliser async_get au lieu de Jquery //Utiliser async_get au lieu de Jquery
return $.ajax({ async_get(
async: true, url_api,
type: "GET", action,
url: url_api, ()=>{},
success: (data, status) => { );
if (status === "success") { }
}
function showStats(data){
const counter = {
"present": {
"total": data["present"],
}, },
error: () => { }, "retard": {
"total": data["retard"],
"justi": data["retard"]["justifie"],
},
"absent": {
"total": data["absent"],
"justi": data["absent"]["justifie"],
}
}
const values = document.querySelector('.stats-values');
values.innerHTML = "";
Object.keys(counter).forEach((key) => {
const item = document.createElement('div');
item.classList.add('stats-values-item');
const div = document.createElement('div');
div.classList.add('stats-values-part');
const withJusti = (key, metric) => {
if (key == "present") return "";
return ` dont ${counter[key].justi[metric]} justifiées`
}
const heure = document.createElement('span');
heure.textContent = `${counter[key].total.heure.toFixed(2)} heure(s)${withJusti(key, "heure")}`;
const demi = document.createElement('span');
demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`;
const jour = document.createElement('span');
jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`;
div.append(jour, demi, heure);
const title = document.createElement('h5');
title.textContent = key.capitalize();
item.append(title, div)
values.appendChild(item);
}); });
const nbAbs = data["absent"]["non_justifie"][assi_metric];
if (nbAbs > assi_seuil) {
document.querySelector('.alerte').classList.remove('invisible');
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
} else {
document.querySelector('.alerte').classList.add('invisible');
}
} }
function countAssiduites(dateDeb, dateFin) { function countAssiduites(dateDeb, dateFin) {
//TODO Utiliser Fetch when plutot que jquery getAssiduitesCount(dateDeb, dateFin, showStats);
$.when(
getAssiduitesCount(dateDeb, dateFin, `etat=present`),
getAssiduitesCount(dateDeb, dateFin, `etat=retard`),
getAssiduitesCount(dateDeb, dateFin, `etat=retard&est_just=v`),
getAssiduitesCount(dateDeb, dateFin, `etat=absent`),
getAssiduitesCount(dateDeb, dateFin, `etat=absent&est_just=v`),
).then(
(pt, rt, rj, at, aj) => {
const counter = {
"present": {
"total": pt[0],
},
"retard": {
"total": rt[0],
"justi": rj[0],
},
"absent": {
"total": at[0],
"justi": aj[0],
}
}
const values = document.querySelector('.stats-values');
values.innerHTML = "";
Object.keys(counter).forEach((key) => {
const item = document.createElement('div');
item.classList.add('stats-values-item');
const div = document.createElement('div');
div.classList.add('stats-values-part');
const withJusti = (key, metric) => {
if (key == "present") return "";
return ` dont ${counter[key].justi[metric]} justifiées`
}
const heure = document.createElement('span');
heure.textContent = `${counter[key].total.heure} heure(s)${withJusti(key, "heure")}`;
const demi = document.createElement('span');
demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`;
const jour = document.createElement('span');
jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`;
div.append(jour, demi, heure);
const title = document.createElement('h5');
title.textContent = key.capitalize();
item.append(title, div)
values.appendChild(item);
});
const nbAbs = counter.absent.total[assi_metric] - counter.absent.justi[assi_metric];
if (nbAbs > assi_seuil) {
document.querySelector('.alerte').classList.remove('invisible');
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
} else {
document.querySelector('.alerte').classList.add('invisible');
}
}
);
} }
function removeAllAssiduites() { function removeAllAssiduites() {
@ -288,105 +304,12 @@
window.addEventListener('load', () => { window.addEventListener('load', () => {
filterAssiduites = {
"columns": [
"entry_date",
"date_debut",
"date_fin",
"etat",
"moduleimpl_id",
"est_just"
],
"filters": {
"etat": [
"retard",
"absent"
],
"moduleimpl_id": "",
"est_just": "false"
}
};
filterJustificatifs = {
"columns": [
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
"etat": [
"attente",
"modifie"
]
}
}
document.getElementById('stats_date_fin').value = assi_date_fin; document.getElementById('stats_date_fin').value = assi_date_fin;
document.getElementById('stats_date_debut').value = assi_date_debut; document.getElementById('stats_date_debut').value = assi_date_debut;
loadAll();
stats(); stats();
}) })
</script> </script>
<style> {% endblock %}
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
.stats {
border: 1px solid #333;
padding: 5px 2px;
width: fit-content;
}
.stats-values {
display: flex;
justify-content: flex-start;
gap: 15px;
}
.stats-values-item h5 {
font-weight: bold;
text-decoration-line: underline;
}
.stats-values-part {
display: flex;
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
border-radius: 7px;
background-color: var(--color-error);
}
.alerte.invisible {
display: none;
}
.alerte p {
font-size: larger;
color: whitesmoke;
}
.suppr {
margin: 5px 0;
}
</style>

View File

@ -1,4 +1,14 @@
{% block pageContent %} {% extends "sco_page.j2" %}
{% block title %}
Calendrier de l'assiduité
{% endblock title %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block app_content %}
{% include "assiduites/widgets/alert.j2" %} {% include "assiduites/widgets/alert.j2" %}
<div class="pageContent"> <div class="pageContent">
@ -250,219 +260,6 @@
} }
.day .dayline {
position: absolute;
display: none;
top: 100%;
z-index: 50;
width: max-content;
height: 75px;
background-color: #dedede;
border-radius: 15px;
padding: 5px;
}
.day:hover .dayline {
display: block;
}
.dayline .mini-timeline {
margin-top: 10%;
}
.dayline-title {
margin: 0;
}
.dayline .mini_tick {
position: absolute;
text-align: center;
top: 0;
transform: translateY(-110%);
z-index: 50;
}
.dayline .mini_tick::after {
display: block;
content: "|";
position: absolute;
bottom: -69%;
z-index: 2;
transform: translateX(200%);
}
#label-nom,
#label-justi {
display: none;
}
.demi .day {
display: flex;
justify-content: space-evenly;
}
.demi .day>span {
display: block;
flex: 1;
text-align: center;
z-index: 1;
width: 100%;
border: 1px solid #d5d5d5;
position: relative;
}
.demi .day>span:first-of-type {
width: 3em;
min-width: 3em;
}
.options>* {
margin-right: 5px;
}
.options input {
margin-right: 6px;
}
.options label {
font-weight: normal;
margin-right: 16px;
}
/*Gestion des bubbles*/
.assiduite-bubble {
position: relative;
display: none;
background-color: #f9f9f9;
border-radius: 5px;
padding: 8px;
border: 3px solid #ccc;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
font-size: 12px;
line-height: 1.4;
z-index: 500;
min-width: max-content;
top: 200%;
}
.mini-timeline-block:hover .assiduite-bubble {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.assiduite-bubble::before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent transparent #f9f9f9 transparent;
}
.assiduite-bubble::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: transparent transparent #ccc transparent;
}
.assiduite-id,
.assiduite-period,
.assiduite-state,
.assiduite-user_id {
margin-bottom: 4px;
}
.assiduite-bubble.absent {
border-color: var(--color-absent) !important;
}
.assiduite-bubble.present {
border-color: var(--color-present) !important;
}
.assiduite-bubble.retard {
border-color: var(--color-retard) !important;
}
/*Gestion des minitimelines*/
.mini-timeline {
height: 7px;
border: 1px solid black;
position: relative;
background-color: white;
}
.mini-timeline.single {
height: 9px;
}
.mini-timeline-block {
position: absolute;
height: 100%;
z-index: 1;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
}
.mini-timeline-block {
cursor: pointer;
}
.mini_tick {
position: absolute;
text-align: start;
top: -40px;
transform: translateX(-50%);
z-index: 50;
}
.mini_tick::after {
display: block;
content: "|";
position: absolute;
bottom: -2px;
z-index: 2;
}
.mini-timeline-block.creneau {
outline: 3px solid var(--color-primary);
pointer-events: none;
}
.mini-timeline-block.absent {
background-color: var(--color-absent) !important;
}
.mini-timeline-block.present {
background-color: var(--color-present) !important;
}
.mini-timeline-block.retard {
background-color: var(--color-retard) !important;
}
.mini-timeline-block.justified {
background-image: var(--motif-justi);
}
.mini-timeline-block.invalid_justified {
background-image: var(--motif-justi-invalide);
}
@media print { @media print {
.couleurs.print { .couleurs.print {
@ -593,4 +390,4 @@
</script> </script>
{% endblock pageContent %} {% endblock app_content %}

View File

@ -1,11 +1,6 @@
{% extends "sco_page.j2" %} {% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %} {% import 'wtf.j2' as wtf %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
{% endblock %}
{% block app_content %} {% block app_content %}
{% for err_msg in form.error_messages %} {% for err_msg in form.error_messages %}
<div class="wtf-error-messages"> <div class="wtf-error-messages">
@ -24,7 +19,3 @@
</form> </form>
{% endblock app_content %} {% endblock app_content %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
{% endblock scripts %}

View File

@ -3,6 +3,7 @@
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
<style> <style>
div.config-section { div.config-section {
font-weight: bold; font-weight: bold;
@ -31,8 +32,18 @@ div.config-section {
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
<script> <script>
$('.timepicker').timepicker({
timeFormat: 'HH:mm',
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
minTime: "00:00",
maxTime: "23:59",
dynamic: false,
dropdown: true,
scrollbar: false
});
function update_test_button_state() { function update_test_button_state() {
var inputValue = document.getElementById('test_edt_id').value; var inputValue = document.getElementById('test_edt_id').value;
document.getElementById('test_load_ics').disabled = inputValue.length === 0; document.getElementById('test_load_ics').disabled = inputValue.length === 0;
@ -78,10 +89,9 @@ c'est à dire à la montre des étudiants.
<div class="col-md-8"> <div class="col-md-8">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }} {{ wtf.form_errors(form, hiddens="only") }}
{{ wtf.form_field(form.assi_morning_time, class="timepicker") }}
{{ wtf.form_field(form.assi_morning_time) }} {{ wtf.form_field(form.assi_lunch_time, class="timepicker") }}
{{ wtf.form_field(form.assi_lunch_time) }} {{ wtf.form_field(form.assi_afternoon_time, class="timepicker") }}
{{ wtf.form_field(form.assi_afternoon_time) }}
{{ wtf.form_field(form.assi_tick_time) }} {{ wtf.form_field(form.assi_tick_time) }}
</div> </div>
</div> </div>

View File

@ -1,3 +1,21 @@
{% extends "sco_page.j2" %}
{% block title %}
Assiduité de {{etud.nomprenom}}
{% endblock title %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
{% endblock %}
{% block app_content %} {% block app_content %}
<div class="pageContent"> <div class="pageContent">

View File

@ -1,94 +0,0 @@
{% block pageContent %}
<div class="pageContent">
<h3>Assiduites et justificatifs de <span class="rouge">{{sem}}</span> </h3>
{% include "assiduites/widgets/tableau_base.j2" %}
<h4>Assiduité :</h4>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<h4>Justificatifs :</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
</div>
<script>
const formsemestre_id = {{ formsemestre_id }};
function getFormSemestreAssiduites(action) {
const path = getUrl() + `/api/assiduites/formsemestre/${formsemestre_id}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
assiduiteCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function getFormSemestreJustificatifs(action) {
const path = getUrl() + `/api/justificatifs/formsemestre/${formsemestre_id}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
justificatifCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function getAssi(action) {
try { getFormSemestreAssiduites(action) } catch (_) { }
}
function getJusti(action) {
try { getFormSemestreJustificatifs(action) } catch (_) { }
}
window.addEventListener('load', () => {
filterJustificatifs = {
"columns": [
"etudid",
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
}
}
filterAssiduites = {
columns: [
"etudid", "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
],
"filters": {
}
}
loadAll();
})
</script>
{% endblock pageContent %}

View File

@ -1,5 +1,34 @@
{#
- TODO : revoir le fonctionnement de cette page (trop lente / complexe)
- Utiliser majoritairement du python
#}
{% extends "sco_page.j2" %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %}
{% block title %}
{{title}}
{% endblock title %}
{% block app_content %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
{% include "assiduites/widgets/toast.j2" %}
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2> <h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
</div>
<h3>{{sem | safe }}</h3> <h3>{{sem | safe }}</h3>
{{diff | safe}} {{diff | safe}}
@ -18,8 +47,14 @@
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne. <p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.
</p> </p>
</div> </div>
{% endblock app_content %}
<script>
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script>
const etudsDefDem = {{ defdem | safe }} const etudsDefDem = {{ defdem | safe }}
const timeMorning = "{{ timeMorning | safe}}"; const timeMorning = "{{ timeMorning | safe}}";
@ -62,14 +97,5 @@
createColumn(); createColumn();
} }
}) })
</script> </script>
{% endblock scripts %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
{% include "assiduites/widgets/toast.j2" %}

View File

@ -1,160 +0,0 @@
{# -*- mode: jinja-html -*- #}
{% include "assiduites/widgets/toast.j2" %}
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
<div id="page-assiduite-content">
{% block content %}
<h2>Signalement de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
<div class="infos">
Date: <span id="datestr"></span>
<input type="text" class="datepicker" name="tl_date" id="tl_date" value="{{ date }}">
</div>
{{timeline|safe}}
<div>
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
<button class="btn" onclick="fastJustify(getCurrentAssiduite(etudid))" id="justif-rapide">Justifier</button>
</div>
<div class="btn_group">
<button class="btn" onclick="setTimeLineTimes({{morning}},{{afternoon}})">Journée</button>
<button class="btn" onclick="setTimeLineTimes({{morning}},{{lunch}})">Matin</button>
<button class="btn" onclick="setTimeLineTimes({{lunch}},{{afternoon}})">Après-midi</button>
</div>
<div class="etud_holder">
<div id="etud_row_{{sco.etud.id}}">
<div class="index"></div>
</div>
</div>
<hr>
{% 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
</h3>
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
</div>
{% endif %}
{{diff | safe}}
<div class="legende">
<h3>Explication de la timeline</h3>
<p>
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
rouge.
<br>
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
le
résolveur de conflit.
<br>
Correspondance des couleurs :
</p>
<ul>
{% include "assiduites/widgets/legende_couleur.j2" %}
</ul>
<p>Vous pouvez justifier rapidement une assiduité en saisisant l'assiduité puis en appuyant sur "Justifier"</p>
<h3>Explication de la saisie différée</h3>
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher
le message d'erreur</p>
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
(préférence de département)</p>
<p>Modifier le module alors que des informations sont déjà enregistrées pour la période changera leur
module.</p>
<p>Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants</p>
<p>Le dernier des boutons retire l'information présente.</p>
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la
colonne.
</p>
</div>
<!-- Ajout d'un conteneur pour le loader -->
<div class="loader-container" id="loaderContainer">
<div class="loader"></div>
</div>
<script>
const etudid = {{ sco.etud.id }};
const nonWorkDays = [{{ nonworkdays| safe }}];
setupDate(() => {
if (updateDate()) {
actualizeEtud(etudid);
updateSelect();
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
onlyAbs();
}
});
setupTimeLine(() => {
if(document.querySelector('.etud_holder .placeholder') != null){
generateAllEtudRow();
}
updateJustifyBtn();
});
window.addEventListener("DOMContentLoaded", () => {
updateDate();
getSingleEtud(etudid);
actualizeEtud(etudid);
updateSelect()
updateJustifyBtn();
})
function setTimeLineTimes(a, b) {
setPeriodValues(a, b);
updateJustifyBtn();
}
window.forceModule = "{{ forcer_module }}"
window.forceModule = window.forceModule == "True" ? true : false
const date_deb = "{{date_deb}}";
const date_fin = "{{date_fin}}";
{% if saisie_eval %}
createColumn(
date_deb,
date_fin,
{{ moduleimpl_id }}
);
window.location.href = "#saisie_eval"
getAndUpdateCol(1)
{% else %}
createColumn();
{% endif %}
</script>
<style>
.justifie {
background-color: rgb(104, 104, 252);
color: whitesmoke;
}
fieldset {
outline: none;
border: none;
}
</style>
{% endblock %}
</div>

View File

@ -1,4 +1,86 @@
{% extends "sco_page.j2" %}
{% block title %}
{{title}}
{% endblock title %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
<script>
{% if readonly != "false" %}
function getPeriodValues(){
return [0, 23]
}
{% endif %}
const nonWorkDays = [{{ nonworkdays| safe }}];
const readOnly = {{ readonly }};
setupDate();
updateDate();
if (!readOnly){
setupTimeLine(()=>{
generateAllEtudRow();
});
}
window.forceModule = "{{ forcer_module }}"
window.forceModule = window.forceModule == "True" ? true : false
const etudsDefDem = {{ defdem | safe }}
const select = document.getElementById("moduleimpl_select");
select?.addEventListener('change', (e) => {
generateAllEtudRow();
});
if (window.forceModule) {
const btn = document.getElementById("validate_selectors");
if (!readOnly && select.value == "") {
document.getElementById('forcemodule').style.display = "block";
}
select?.addEventListener('change', (e) => {
if (e.target.value != "") {
document.getElementById('forcemodule').style.display = "none";
} else {
document.getElementById('forcemodule').style.display = "block";
}
});
}
</script>
{% endblock scripts %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
{% endblock styles %}
{% block app_content %}
{% include "assiduites/widgets/toast.j2" %} {% include "assiduites/widgets/toast.j2" %}
{{ minitimeline|safe }}
<section id="content"> <section id="content">
<div class="no-display"> <div class="no-display">
@ -20,7 +102,7 @@
<div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div> <div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div>
<div class="infos-button" style="margin-left: 24px;">Date&nbsp;: <span style="margin-left: 8px;" <div class="infos-button" style="margin-left: 24px;">Date&nbsp;: <span style="margin-left: 8px;"
id="datestr"></span> id="datestr"></span>
<input type="text" class="datepicker" name="tl_date" id="tl_date" value="{{ date }}" <input type="text" name="tl_date" id="tl_date" value="{{ date }}"
onchange="updateDate()"> onchange="updateDate()">
</div> </div>
</div> </div>
@ -47,7 +129,6 @@
Faire la saisie Faire la saisie
</button> </button>
{% endif %} {% endif %}
<p>Utilisez le bouton "Actualiser" si vous modifier la date ou le(s) groupe(s) sélectionné(s)</p>
<div class="etud_holder"> <div class="etud_holder">
@ -79,57 +160,6 @@
{% include "assiduites/widgets/prompt.j2" %} {% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %} {% include "assiduites/widgets/conflict.j2" %}
<script> </section>
{% if readonly != "false" %} {% endblock app_content %}
function getPeriodValues(){
return [0, 23]
}
{% endif %}
const nonWorkDays = [{{ nonworkdays| safe }}];
const readOnly = {{ readonly }};
setupDate();
updateDate();
if (!readOnly){
setupTimeLine(()=>{
if(document.querySelector('.etud_holder .placeholder') != null){
generateAllEtudRow();
}
});
}
window.forceModule = "{{ forcer_module }}"
window.forceModule = window.forceModule == "True" ? true : false
const etudsDefDem = {{ defdem | safe }}
const select = document.getElementById("moduleimpl_select");
select?.addEventListener('change', (e) => {
generateAllEtudRow();
});
if (window.forceModule) {
const btn = document.getElementById("validate_selectors");
if (!readOnly && select.value == "") {
document.getElementById('forcemodule').style.display = "block";
}
select?.addEventListener('change', (e) => {
if (e.target.value != "") {
document.getElementById('forcemodule').style.display = "none";
} else {
document.getElementById('forcemodule').style.display = "block";
}
});
}
</script>
</section>

View File

@ -10,11 +10,11 @@
{% if action == "modifier" %} {% if action == "modifier" %}
{% include "assiduites/widgets/tableau_actions/modifier.j2" %} {% include "assiduites/widgets/tableau_actions/modifier.j2" %}
{% else%} {% else %}
{% include "assiduites/widgets/tableau_actions/details.j2" %} {% include "assiduites/widgets/tableau_actions/details.j2" %}
{% endif %} {% endif %}
{% if not current_user.has_permission(sco.Permission.AbsJustifView)%} {% if not current_user.has_permission(sco.Permission.AbsJustifView) %}
<div class="help fontred" style="margin-top: 16px;"> <div class="help fontred" style="margin-top: 16px;">
Vous n'avez pas la permission d'ouvrir les fichiers justificatifs Vous n'avez pas la permission d'ouvrir les fichiers justificatifs
déposés par d'autres personnes. déposés par d'autres personnes.
@ -22,7 +22,7 @@
{% endif %} {% endif %}
<div style="margin-top: 32px;"> <div style="margin-top: 32px;">
<a href="" id="lien-retour">retour</a> <a class="stdlink" href="" id="lien-retour">retour</a>
</div> </div>
<script> <script>
window.addEventListener('load', () => { window.addEventListener('load', () => {

View File

@ -3,5 +3,6 @@
<div class="assiduite-period">{{date_debut}}</div> <div class="assiduite-period">{{date_debut}}</div>
<div class="assiduite-period">{{date_fin}}</div> <div class="assiduite-period">{{date_fin}}</div>
<div class="assiduite-state">État: {{etat}}</div> <div class="assiduite-state">État: {{etat}}</div>
<div class="assiduite-why">Motif: {{motif}}</div>
<div class="assiduite-user_id">{{saisie}}</div> <div class="assiduite-user_id">{{saisie}}</div>
</div> </div>

View File

@ -6,8 +6,8 @@
*/ */
function getLeftPosition(start) { function getLeftPosition(start) {
const startTime = new Date(start); const startTime = new Date(start);
const startMins = (startTime.getHours() - 8) * 60 + startTime.getMinutes(); const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%"; return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
} }
/** /**
* Ajustement de l'espacement vertical entre les assiduités superposées * Ajustement de l'espacement vertical entre les assiduités superposées
@ -76,13 +76,7 @@
const duration = (endTime - startTime) / 1000 / 60; const duration = (endTime - startTime) / 1000 / 60;
const percent = (duration / (18 * 60 - 8 * 60)) * 100 const percent = (duration / (t_end * 60 - t_start * 60)) * 100
if (percent > 100) {
console.log(start, end);
console.log(startTime, endTime)
}
return percent + "%"; return percent + "%";
} }
@ -162,6 +156,13 @@
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() }) document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
// fermeture du modal en appuyant sur echap
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close()
}
}, { once: true })
this.render() this.render()
} }
@ -246,12 +247,11 @@
*/ */
splitAssiduiteModal() { splitAssiduiteModal() {
//Préparation du prompt //Préparation du prompt
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend> const htmlPrompt = `<legend>Entrez l'heure de séparation</legend>
<input type="time" id="promptTime" name="appt" <input type="text" id="promptTime" name="appt"required style="position: relative; z-index: 100000;">`;
min="08:00" max="18:00" required>`;
const fieldSet = document.createElement("fieldset"); const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit"); fieldSet.classList.add("fieldsplit", "timepicker");
fieldSet.innerHTML = htmlPrompt; fieldSet.innerHTML = htmlPrompt;
//Callback de division //Callback de division
@ -309,11 +309,28 @@
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée." "L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
); );
openAlertModal("Attention", att, "", "var(--color-warning))"); openAlertModal("Attention", att, "", "var(--color-warning)");
} }
}; };
openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)"); openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
// Initialisation du timepicker
const deb = this.selectedAssiduite.date_debut.substring(11,16);
const fin = this.selectedAssiduite.date_fin.substring(11,16);
setTimeout(()=>{
$('#promptTime').timepicker({
timeFormat: 'HH:mm',
interval: 60 * tick_delay,
minTime: deb,
startTime: deb,
maxTime: fin,
dynamic: false,
dropdown: true,
scrollbar: false,
});
}, 100
);
} }
/** /**
@ -371,8 +388,7 @@
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>'; assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
// Ajout des labels d'heure sur la frise chronologique // Ajout des labels d'heure sur la frise chronologique
// TODO permettre la modification des bornes (8 et 18) for (let i = t_start; i <= t_end; i++) {
for (let i = 8; i <= 18; i++) {
const timeLabel = document.createElement("div"); const timeLabel = document.createElement("div");
timeLabel.className = "time-label"; timeLabel.className = "time-label";
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`; timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
@ -459,4 +475,9 @@
this.editBtn.disabled = true; this.editBtn.disabled = true;
} }
} }
</script> </script>
<style>
.ui-timepicker-container {
z-index: 100000 !important;
}
</style>

View File

@ -270,6 +270,10 @@
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
border: 10px solid white; border: 10px solid white;
} }
.mini-form {
color: black;
}
</style> </style>
<script> <script>
@ -315,7 +319,7 @@
<input disabled="" type="radio" class="rbtn retard" name="mass_action_${col_id}" value="retard" onclick="massCol(this)"> <input disabled="" type="radio" class="rbtn retard" name="mass_action_${col_id}" value="retard" onclick="massCol(this)">
<input disabled="" type="radio" class="rbtn absent" name="mass_action_${col_id}" value="absent" onclick="massCol(this)"> <input disabled="" type="radio" class="rbtn absent" name="mass_action_${col_id}" value="absent" onclick="massCol(this)">
<input disabled="" type="radio" class="rbtn aucun" name="mass_action_${col_id}" value="remove" onclick="massCol(this)"> <input disabled="" type="radio" class="rbtn aucun" name="mass_action_${col_id}" value="remove" onclick="massCol(this)">
</div> </div>
</div> </div>
`; `;

View File

@ -73,11 +73,6 @@
updateSelectedSelect(getCurrentAssiduiteModuleImplId()); updateSelectedSelect(getCurrentAssiduiteModuleImplId());
updateJustifyBtn(); updateJustifyBtn();
} }
try {
if (isCalendrier()) {
window.location = `liste_assiduites_etud?etudid=${etudid}&assiduite_id=${assiduité.assiduite_id}`
}
} catch { }
}); });
//ajouter affichage assiduites on over //ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité); setupAssiduiteBuble(block, assiduité);
@ -138,51 +133,48 @@
*/ */
function setupAssiduiteBuble(el, assiduite) { function setupAssiduiteBuble(el, assiduite) {
if (!assiduite) return; if (!assiduite) return;
el.addEventListener("mouseenter", (event) => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.className = "assiduite-bubble";
bubble.classList.add("is-active", assiduite.etat.toLowerCase());
bubble.innerHTML = ""; const bubble = document.createElement('div');
bubble.className = "assiduite-bubble";
bubble.classList.add(assiduite.etat.toLowerCase());
const idDiv = document.createElement("div"); const idDiv = document.createElement("div");
idDiv.className = "assiduite-id"; idDiv.className = "assiduite-id";
idDiv.textContent = `${getModuleImpl(assiduite)}`; idDiv.textContent = `${getModuleImpl(assiduite)}`;
bubble.appendChild(idDiv); bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div"); const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period"; periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`; periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb); bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div"); const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period"; periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`; periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin); bubble.appendChild(periodDivFin);
const stateDiv = document.createElement("div"); const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state"; stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`; stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv); bubble.appendChild(stateDiv);
const userIdDiv = document.createElement("div"); const motifDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id"; stateDiv.className = "assiduite-why";
userIdDiv.textContent = `saisie le ${formatDateModal( stateDiv.textContent = `Motif: ${assiduite.desc?.capitalize()}`;
assiduite.entry_date, bubble.appendChild(motifDiv);
" à "
)}`;
if (assiduite.user_id != null) { const userIdDiv = document.createElement("div");
userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}` userIdDiv.className = "assiduite-user_id";
} userIdDiv.textContent = `saisie le ${formatDateModal(
bubble.appendChild(userIdDiv); assiduite.entry_date,
" à "
)}`;
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`; if (assiduite.user_id != null) {
bubble.style.top = `${event.clientY + 20}px`; userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}`
}); }
el.addEventListener("mouseout", () => { bubble.appendChild(userIdDiv);
const bubble = document.querySelector(".assiduite-bubble");
bubble.classList.remove("is-active"); el.appendChild(bubble);
});
} }
function setMiniTick(timelineDate, dayStart, dayDuration) { function setMiniTick(timelineDate, dayStart, dayDuration) {
@ -198,127 +190,4 @@
return tick return tick
} }
</script> </script>
<style>
.assiduite-bubble {
position: fixed;
display: none;
background-color: #f9f9f9;
border-radius: 5px;
padding: 8px;
border: 3px solid #ccc;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
font-size: 12px;
line-height: 1.4;
z-index: 500;
}
.assiduite-bubble.is-active {
display: block;
}
.assiduite-bubble::before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent transparent #f9f9f9 transparent;
}
.assiduite-bubble::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 5px;
border-style: solid;
border-color: transparent transparent #ccc transparent;
}
.assiduite-id,
.assiduite-period,
.assiduite-state,
.assiduite-user_id {
margin-bottom: 4px;
}
.assiduite-bubble.absent {
border-color: var(--color-absent) !important;
}
.assiduite-bubble.present {
border-color: var(--color-present) !important;
}
.assiduite-bubble.retard {
border-color: var(--color-retard) !important;
}
.mini-timeline {
height: 7px;
border: 1px solid black;
position: relative;
background-color: white;
}
.mini-timeline.single {
height: 9px;
}
.mini-timeline-block {
position: absolute;
height: 100%;
z-index: 1;
}
#page-assiduite-content .mini-timeline-block {
cursor: pointer;
}
.mini_tick {
position: absolute;
text-align: start;
top: -40px;
transform: translateX(-50%);
z-index: 1;
}
.mini_tick::after {
display: block;
content: "|";
position: absolute;
bottom: -2px;
z-index: 2;
}
.mini-timeline-block.creneau {
outline: 3px solid var(--color-primary);
pointer-events: none;
}
.mini-timeline-block.absent {
background-color: var(--color-absent) !important;
}
.mini-timeline-block.present {
background-color: var(--color-present) !important;
}
.mini-timeline-block.retard {
background-color: var(--color-retard) !important;
}
.mini-timeline-block.justified {
background-image: var(--motif-justi);
}
.mini-timeline-block.invalid_justified {
background-image: var(--motif-justi-invalide);
}
</style>

View File

@ -1,156 +0,0 @@
<div>
{% if label != false%}
<label for="moduleimpl_select">
Module
</label>
{% else %}
{% endif %}
{% if moduleid %}
<select id="{{moduleid}}" class="dynaSelect">
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
</select>
{% else %}
<select id="moduleimpl_select" class="dynaSelect">
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
</select>
{% endif %}
<div id="saved" style="display: none;">
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
</div>
</div>
<script>
function getEtudFormSemestres() {
let semestre = {};
sync_get(getUrl() + `/api/etudiant/etudid/${etudid}/formsemestres`, (data) => {
semestre = data;
});
return semestre;
}
function filterFormSemestres(semestres, dateIso) {
const date = new Date(Date.removeUTC(dateIso));
semestres = semestres.filter((fm) => {
return date.isBetween(new Date(Date.removeUTC(fm.date_debut_iso)), new Date(Date.removeUTC(fm.date_fin_iso)), '[]');
})
return semestres;
}
function getFormSemestreProgramme(fm_id) {
let semestre = {};
sync_get(getUrl() + `/api/formsemestre/${fm_id}/programme`, (data) => {
semestre = data;
});
return semestre;
}
function getModulesImplByFormsemestre(semestres) {
const map = new Map();
semestres.forEach((fm) => {
const array = [];
const fm_p = getFormSemestreProgramme(fm.formsemestre_id);
["ressources", "saes", "modules"].forEach((r) => {
if (r in fm_p) {
fm_p[r].forEach((o) => {
array.push(getModuleInfos(o))
})
}
})
map.set(fm.titre_num, array)
})
return map;
}
function getModuleInfos(obj) {
return {
moduleimpl_id: obj.moduleimpl_id,
titre: obj.module.titre,
code: obj.module.code,
}
}
function populateSelect(sems, selected, query) {
const select = document.querySelector(query);
select.innerHTML = document.getElementById('saved').innerHTML
sems.forEach((mods, label) => {
const optGrp = document.createElement('optgroup');
optGrp.label = label
mods.forEach((obj) => {
const opt = document.createElement('option');
opt.value = obj.moduleimpl_id;
opt.textContent = `${obj.code} ${obj.titre}`
if (obj.moduleimpl_id == selected) {
opt.setAttribute('selected', 'true');
}
optGrp.appendChild(opt);
})
select.appendChild(optGrp);
})
if (selected === "autre") {
select.querySelector('option[value="autre"]').setAttribute('selected', 'true');
}
}
function updateSelect(moduleimpl_id, query = "#moduleimpl_select", dateIso = null) {
let sem = getEtudFormSemestres()
if (!dateIso) {
dateIso = getDate().format("YYYY-MM-DD")
}
sem = filterFormSemestres(sem, dateIso)
const mod = getModulesImplByFormsemestre(sem)
populateSelect(mod, moduleimpl_id, query);
}
function updateSelectedSelect(moduleimpl_id, query = "#moduleimpl_select") {
const mod_id = moduleimpl_id != null ? moduleimpl_id : ""
document.querySelector(query).value = `${mod_id}`.toLowerCase();
}
{% if moduleid %}
const moduleimpl_dynamic_selector_id = "{{moduleid}}"
{% else %}
const moduleimpl_dynamic_selector_id = "moduleimpl_select"
{% endif %}
window.addEventListener("load", () => {
document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => {
const assi = getCurrentAssiduite(etudid);
if (assi) {
editAssiduite(assi.assiduite_id, assi.etat, [assi]);
}
})
try {
const conflicts = getAssiduitesConflict(etudid);
if (conflicts.length > 0) {
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
}
} catch { }
}, { once: true });
</script>
<style>
#moduleimpl_select {
width: 125px;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,20 @@
<select name="moduleimpl_select" id="moduleimpl_select">
{% with moduleimpl_id=moduleimpl_id %}
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
{% endwith %}
{% for cat, mods in choices.items() %}
<optgroup label="{{cat}}">
{% for mod in mods %}
{% if mod.moduleimpl_id == moduleimpl_id %}
<option value="{{mod.moduleimpl_id}}" selected> {{mod.name}} </option>
{% else %}
<option value="{{mod.moduleimpl_id}}"> {{mod.name}} </option>
{% endif %}
{% endfor %}
</optgroup>
{% endfor %}
</select>

View File

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

View File

@ -1,6 +1,6 @@
<div> <div>
<div class="sco_box_title">{{ titre }}</div> <div class="sco_box_title">{{ titre }}</div>
<div id="options-tableau"> <div class="options-tableau">
{% if afficher_options != false %} {% if afficher_options != false %}
<input type="checkbox" id="show_pres" name="show_pres" <input type="checkbox" id="show_pres" name="show_pres"
onclick="updateTableau()" {{'checked' if options.show_pres else ''}}> onclick="updateTableau()" {{'checked' if options.show_pres else ''}}>
@ -17,33 +17,133 @@
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a> <a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
<br> <br>
{% endif %} {% endif %}
<label for="nb_ligne_page">Nombre de lignes par page : </label> <label for="nb_ligne_page">Nombre de lignes par page :</label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page" <select name="nb_ligne_page" id="nb_ligne_page" onchange="updateTableau()">
size="4" step="25" min="10" value="{{options.nb_ligne_page}}" {% for i in [25,50,100,1000] %}
onchange="updateTableau()" {% if i == options.nb_ligne_page %}
> <option selected value="{{i}}">{{i}}</option>
{% else %}
<label for="n_page">Page n°</label> <option value="{{i}}">{{i}}</option>
<select name="n_page" id="n_page"> {% endif %}
{% for n in range(1,total_pages+1) %}
<option value="{{n}}" {{'selected' if n == options.page else ''}}>{{n}}</option>
{% endfor %} {% endfor %}
</select> </select>
<br> <br>
</div> </div>
<div class="div-tableau">
<div class="options-tableau">
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
<!-- Mettre les flèches -->
{% if total_pages > 1 %}
<ul class="pagination">
<li class="">
<a onclick="navigateToPage({{options.page - 1}})">&lt;</a>
</li>
<!-- Toujours afficher la première page -->
<li class="{% if options.page == 1 %}active{% endif %}">
<a onclick="navigateToPage({{1}})">1</a>
</li>
<!-- Afficher les ellipses si la page courante est supérieure à 2 -->
<!-- et qu'il y a plus d'une page entre le 1 et la page courante-1 -->
{% if options.page > 2 and (options.page - 1) - 1 > 1 %}
<li class="disabled"><span>...</span></li>
{% endif %}
<!-- Afficher la page précédente, la page courante, et la page suivante -->
{% for i in range(options.page - 1, options.page + 2) %}
{% if i > 1 and i < total_pages %}
<li class="{% if options.page == i %}active{% endif %}">
<a onclick="navigateToPage({{i}})">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
<!-- Afficher les ellipses si la page courante est inférieure à l'avant-dernière page -->
<!-- et qu'il y a plus d'une page entre le total_pages et la page courante+1 -->
{% if options.page < total_pages - 1 and total_pages - (options.page + 1 ) > 1 %}
<li class="disabled"><span>...</span></li>
{% endif %}
<!-- Toujours afficher la dernière page -->
<li class="{% if options.page == total_pages %}active{% endif %}">
<a onclick="navigateToPage({{total_pages}})">{{ total_pages }}</a>
</li>
<li class="">
<a onclick="navigateToPage({{options.page + 1}})">&gt;</a>
</li>
</ul>
{% else %}
<!-- Afficher un seul bouton si il n'y a qu'une seule page -->
<ul class="pagination">
<li class="active"><a onclick="navigateToPage({{1}})">1</a></li>
</ul>
{% endif %}
</div>
{{table.html() | safe}}
<div class="options-tableau">
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
<!-- Mettre les flèches -->
{% if total_pages > 1 %}
<ul class="pagination">
<li class="">
<a onclick="navigateToPage({{options.page - 1}})">&lt;</a>
</li>
<!-- Toujours afficher la première page -->
<li class="{% if options.page == 1 %}active{% endif %}">
<a onclick="navigateToPage({{1}})">1</a>
</li>
<!-- Afficher les ellipses si la page courante est supérieure à 2 -->
<!-- et qu'il y a plus d'une page entre le 1 et la page courante-1 -->
{% if options.page > 2 and (options.page - 1) - 1 > 1 %}
<li class="disabled"><span>...</span></li>
{% endif %}
<!-- Afficher la page précédente, la page courante, et la page suivante -->
{% for i in range(options.page - 1, options.page + 2) %}
{% if i > 1 and i < total_pages %}
<li class="{% if options.page == i %}active{% endif %}">
<a onclick="navigateToPage({{i}})">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
<!-- Afficher les ellipses si la page courante est inférieure à l'avant-dernière page -->
<!-- et qu'il y a plus d'une page entre le total_pages et la page courante+1 -->
{% if options.page < total_pages - 1 and total_pages - (options.page + 1 ) > 1 %}
<li class="disabled"><span>...</span></li>
{% endif %}
<!-- Toujours afficher la dernière page -->
<li class="{% if options.page == total_pages %}active{% endif %}">
<a onclick="navigateToPage({{total_pages}})">{{ total_pages }}</a>
</li>
<li class="">
<a onclick="navigateToPage({{options.page + 1}})">&gt;</a>
</li>
</ul>
{% else %}
<!-- Afficher un seul bouton si il n'y a qu'une seule page -->
<ul class="pagination">
<li class="active"><a onclick="navigateToPage({{1}})">1</a></li>
</ul>
{% endif %}
</div>
</div> </div>
{{table.html() | safe}} </div>
<script> <script>
function updateTableau() { function updateTableau() {
const url = new URL(location.href); const url = new URL(location.href);
const form = document.getElementById("options-tableau"); const formValues = document.querySelectorAll(".options-tableau *[name]");
const formValues = form.querySelectorAll("*[name]");
formValues.forEach((el) => { formValues.forEach((el) => {
if (el.type == "checkbox") { if (el.type == "checkbox") {
url.searchParams.set(el.name, el.checked) url.searchParams.set(el.name, el.checked)
@ -58,10 +158,56 @@
} }
} }
const total_pages = {{total_pages}};
function navigateToPage(pageNumber){
if(pageNumber > total_pages || pageNumber < 1) return;
const url = new URL(location.href);
url.searchParams.set("n_page", pageNumber)
if (!url.href.endsWith("#options-tableau")) {
location.href = url.href + "#options-tableau";
} else {
location.href = url.href;
}
}
window.addEventListener('load', ()=>{
const table_columns = [...document.querySelectorAll('.external-sort')];
table_columns.forEach((e)=>e.addEventListener('click', ()=>{
// récupération de l'ordre "ascending" / "descending"
let order = e.ariaSort;
// récupération de la colonne à ordonner
// il faut avoir une classe `external-type:<NOM COL>`
let order_col = e.className.split(" ").find((e)=>e.indexOf("external-type:") != -1);
//Création de la nouvelle url avec le tri
const url = new URL(location.href);
url.searchParams.set("order", order);
url.searchParams.set("order_col", order_col.split(":")[1]);
location.href = url.href
}));
});
</script> </script>
<style> <style>
.small-font { .small-font {
font-size: 9pt; font-size: 9pt;
} }
.div-tableau{
display: flex;
flex-direction: column;
align-items: center;
max-width: fit-content;
}
.pagination li{
cursor: pointer;
}
</style> </style>

View File

@ -1,13 +1,36 @@
<h2>Détails {{type}}</h2> <h2>Détails {{type}} concernant <span class="etudinfo"
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span></h2>
<style>
.info-row {
margin-top: 12px;
}
.info-label {
font-weight: bold;
}
.info-etat {
font-size: 110%;
font-weight: bold;
background-color: rgb(253, 234, 210);
border: 1px solid grey;
border-radius: 4px;
padding: 4px;
}
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style>
<div id="informations"> <div id="informations">
<div class="info-row">
<span class="info-label">Étudiant{{etud.e}} concerné{{etud.e}}:</span> <span class="etudinfo" <div class="info-saisie">
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span> <span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Période :</span> {{objet.date_debut}} au {{objet.date_fin}} <span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b>
</div> </div>
{% if type == "Assiduité" %} {% if type == "Assiduité" %}
@ -23,27 +46,27 @@
{% else %} {% else %}
<span class="info-label">État de l'assiduité :</span> <span class="info-label">État de l'assiduité :</span>
{% endif %} {% endif %}
<b>{{objet.etat}}</b> <span class="info-etat">{{objet.etat}}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
{% if type == "Justificatif" %} {% if type == "Justificatif" %}
<div class="info-label">Raison:</div> <span class="info-label">Raison:</span>
{% if objet.raison != None %} {% if can_view_justif_detail %}
<div class="text">{{objet.raison}}</div> <span class="text">{{objet.raison or " "}}</span>
{% else %}
<span class="text unauthorized">(cachée)</span>
{% endif %}
{% else %} {% else %}
<div class="text">/div> <span class="info-label">Description:</span>
{% endif %}
{% else %}
<div class="info-label">Description:</div>
{% if objet.description != None %} {% if objet.description != None %}
<div class="text">{{objet.description}}</div> <span class="text">{{objet.description}}</span>
{% else %} {% else %}
<div class="text"></div> <span class="text"></span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </span>
</div> </div>
{# Affichage des justificatifs si assiduité justifiée #} {# Affichage des justificatifs si assiduité justifiée #}
@ -54,7 +77,8 @@
<span class="text">Oui</span> <span class="text">Oui</span>
<div> <div>
{% for justi in objet.justification.justificatifs %} {% 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)}}" <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> target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
{% endfor %} {% endfor %}
</div> </div>
@ -69,13 +93,15 @@
<div class="info-row"> <div class="info-row">
<span class="info-label">Assiduités concernées: </span> <span class="info-label">Assiduités concernées: </span>
{% if objet.justification.assiduites %} {% if objet.justification.assiduites %}
<div> <ul>
{% for assi in objet.justification.assiduites %} {% 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)}}" <li><a href="{{url_for('assiduites.tableau_assiduite_actions',
target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au 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> {{assi.date_fin}}</a>
</li>
{% endfor %} {% endfor %}
</div> </ul>
{% else %} {% else %}
<span class="text">Aucune</span> <span class="text">Aucune</span>
{% endif %} {% endif %}
@ -84,27 +110,31 @@
{# Affichage des fichiers des justificatifs #} {# Affichage des fichiers des justificatifs #}
{% if type == "Justificatif"%} {% if type == "Justificatif"%}
<div class="info-row"> <div class="info-row">
<span class="info-label">Fichiers enregistrés: </span> <span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %} {% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div> <div>Total : {{objet.justification.fichiers.total}} </div>
<ul> <ul>
{% for filename in objet.justification.fichiers.filenames %} {% for filename in objet.justification.fichiers.filenames %}
<li> <li>
<a <a
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a> href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,
</li> filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
{% endfor %} </li>
{% if not objet.justification.fichiers.filenames %} {% endfor %}
<li class="fontred">fichiers non visibles</li> {% if not objet.justification.fichiers.filenames %}
<li class="fontred">fichiers non visibles</li>
{% endif %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %} {% endif %}
</ul> </div>
{% else %} {% if current_user.has_permission(sco.Permission.AbsChange) %}
<span class="text">Aucun</span> <div><a class="stdlink" href="{{
url_for('assiduites.edit_justificatif_etud', scodoc_dept=g.scodoc_dept, justif_id=obj_id)
}}">modifier ce justificatif</a>
</div>
{% endif %} {% endif %}
</div>
{% endif %} {% endif %}
</div>
<div class="info-row">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>

View File

@ -1,5 +1,7 @@
<h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2> <h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2>
{# XXX cette page ne semble plus utile ! remplacée par edit_justificatif_etud #}
<div> <div>
Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}} Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}}
</div> </div>
@ -39,8 +41,12 @@ Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_de
<option value="modifie">Modifié</option> <option value="modifie">Modifié</option>
</select> </select>
<legend for="raison">Raison</legend> {% if current_user.has_permission(sco.Permission.AbsJustifView) %}
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea> <legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
{% else %}
<div class="unauthorized">(raison non visible ni modifiable)</div>
{% endif %}
<legend>Fichiers</legend> <legend>Fichiers</legend>

View File

@ -1,465 +0,0 @@
<table id="assiduiteTable">
<thead>
<tr>
<th>
<div>
<span>Début</span>
<a class="icon order" onclick="order('date_debut', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>Fin</span>
<a class="icon order" onclick="order('date_fin', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>État</span>
<a class="icon order" onclick="order('etat', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>Module</span>
<a class="icon order" onclick="order('moduleimpl_id', assiduiteCallBack, this)"></a>
</div>
</th>
<th>
<div>
<span>Justifiée</span>
<a class="icon order" onclick="order('est_just', assiduiteCallBack, this)"></a>
</div>
</th>
</tr>
</thead>
<tbody id="tableBodyAssiduites">
</tbody>
</table>
<div id="paginationContainerAssiduites" class="pagination-container">
</div>
<div style="display: none;" id="cache-module">
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
</div>
<script>
const paginationContainerAssiduites = document.getElementById("paginationContainerAssiduites");
let currentPageAssiduites = 1;
let orderAssiduites = true;
let filterAssiduites = {
columns: [
"entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
],
filters: {}
}
const tableBodyAssiduites = document.getElementById("tableBodyAssiduites");
function assiduiteCallBack(assi) {
assi = filterArray(assi, filterAssiduites.filters)
renderTableAssiduites(currentPageAssiduites, assi);
renderPaginationButtons(assi);
try { stats() } catch (_) { }
}
function renderTableAssiduites(page, assiduités) {
generateTableHead(filterAssiduites.columns, true)
tableBodyAssiduites.innerHTML = "";
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
assiduités.slice(start, end).forEach((assiduite) => {
const row = document.createElement("tr");
row.setAttribute('type', "assiduite");
row.setAttribute('obj_id', assiduite.assiduite_id);
const etat = assiduite.etat.toLowerCase();
row.classList.add(`l-${etat}`);
filterAssiduites.columns.forEach((k) => {
const td = document.createElement('td');
if (k.indexOf('date') != -1) {
td.textContent = new Date(Date.removeUTC(assiduite[k])).format(`DD/MM/Y HH:mm`)
} else if (k.indexOf("module") != -1) {
td.textContent = getModuleImpl(assiduite);
} else if (k.indexOf('est_just') != -1) {
td.textContent = assiduite[k] ? "Oui" : "Non"
if (assiduite[k]) row.classList.add("est_just")
} else if (k.indexOf('etudid') != -1) {
const e = getEtudiant(assiduite.etudid);
td.innerHTML = `<a class="etudinfo" id="line-${assiduite.etudid}" href="bilan_etud?etudid=${assiduite.etudid}">${e.prenom.capitalize()} ${e.nom.toUpperCase()}</a>`;
} else {
td.textContent = assiduite[k].capitalize()
}
row.appendChild(td)
})
row.addEventListener("contextmenu", openContext);
tableBodyAssiduites.appendChild(row);
});
updateActivePaginationButton();
}
function detailAssiduites(assiduite_id) {
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
async_get(
path,
(data) => {
const user = getUser(data);
const module = getModuleImpl(data);
const date_debut = new Date(Date.removeUTC(data.date_debut)).format("DD/MM/YYYY HH:mm");
const date_fin = new Date(Date.removeUTC(data.date_fin)).format("DD/MM/YYYY HH:mm");
const entry_date = new Date(Date.removeUTC(data.entry_date)).format("DD/MM/YYYY HH:mm");
const etat = data.etat.capitalize();
const desc = data.desc == null ? "" : data.desc;
const id = data.assiduite_id;
const est_just = data.est_just ? "Oui" : "Non";
const html = `
<div class="obj-detail">
<div class="obj-dates">
<div id="date_debut" class="obj-part">
<span class="obj-title">Date de début</span>
<span class="obj-content">${date_debut}</span>
</div>
<div id="date_fin" class="obj-part">
<span class="obj-title">Date de fin</span>
<span class="obj-content">${date_fin}</span>
</div>
<div id="entry_date" class="obj-part">
<span class="obj-title">Date de saisie</span>
<span class="obj-content">${entry_date}</span>
</div>
</div>
<div class="obj-mod">
<div id="module" class="obj-part">
<span class="obj-title">Module</span>
<span class="obj-content">${module}</span>
</div>
<div id="etat" class="obj-part">
<span class="obj-title">Etat</span>
<span class="obj-content">${etat}</span>
</div>
<div id="user" class="obj-part">
<span class="obj-title">par</span>
<span class="obj-content">${user}</span>
</div>
</div>
<div class="obj-rest">
<div id="est_just" class="obj-part">
<span class="obj-title">Justifié</span>
<span class="obj-content">${est_just}</span>
</div>
<div id="desc" class="obj-part">
<span class="obj-title">Description</span>
<p class="obj-content">${desc}</p>
</div>
<div id="id" class="obj-part" data-assiduite-id="${id}">
</div>
</div>
</div>
`
const el = document.createElement('div');
el.innerHTML = html;
openAlertModal("Détails", el.firstElementChild, null, "var(--color-information)")
}
)
}
function editionAssiduites(assiduite_id) {
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
async_get(
path,
(data) => {
let module = data.moduleimpl_id;
if (
module == null && data.hasOwnProperty("external_data") &&
data.external_data != null &&
data.external_data.hasOwnProperty('module')
) {
module = data.external_data.module.toLowerCase();
}
const etat = data.etat;
let desc = data.desc == null ? "" : data.desc;
const html = `
<div class="assi-edit">
<div class="assi-edit-part">
<legend>État de l'assiduité</legend>
<select name="etat" id="etat">
<option value="present">Présent</option>
<option value="retard">En Retard</option>
<option value="absent">Absent</option>
</select>
</div>
<div class="assi-edit-part">
<legend>Module</legend>
<select name="module" id="module">
</select>
</div>
<div class="assi-edit-part">
<legend>Description</legend>
<textarea name="desc" id="desc" cols="50" rows="10" maxlength="500"></textarea>
</div>
</div>
`
const el = document.createElement('div')
el.innerHTML = html;
const assiEdit = el.firstElementChild;
assiEdit.querySelector('#etat').value = etat.toLowerCase();
assiEdit.querySelector('#desc').value = desc != null ? desc : "";
updateSelect(module, '#moduleimpl_select', data.date_debut.split('T')[0])
assiEdit.querySelector('#module').replaceWith(document.querySelector('#moduleimpl_select').cloneNode(true));
openPromptModal("Modification de l'assiduité", assiEdit, () => {
const prompt = document.querySelector('.assi-edit');
const etat = prompt.querySelector('#etat').value;
const desc = prompt.querySelector('#desc').value;
let module = prompt.querySelector('#moduleimpl_select').value;
let edit = {
"etat": etat,
"desc": desc,
"external_data": data.external_data
}
edit = setModuleImplId(edit, module);
fullEditAssiduites(data.assiduite_id, edit, () => {
loadAll();
})
}, () => { }, "var(--color-information)");
}
);
}
function fullEditAssiduites(assiduite_id, obj, call = () => { }) {
const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
async_post(
path,
obj,
call,
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
function filterAssi() {
let html = `
<div class="filter-body">
<h3>Affichage des colonnes:</h3>
<div class="filter-head">
<label>
Date de saisie
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
</label>
<label>
Date de Début
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
</label>
<label>
Date de Fin
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
</label>
<label>
Etat
<input class="chk" type="checkbox" name="etat" id="etat" checked>
</label>
<label>
Module
<input class="chk" type="checkbox" name="moduleimpl_id" id="moduleimpl_id" checked>
</label>
<label>
Justifiée
<input class="chk" type="checkbox" name="est_just" id="est_just" checked>
</label>
</div>
<hr>
<h3>Filtrage des colonnes:</h3>
<span class="filter-line">
<span class="filter-title" for="entry_date">Date de saisie</span>
<select name="entry_date_pref" id="entry_date_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_debut">Date de début</span>
<select name="date_debut_pref" id="date_debut_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_fin">Date de fin</span>
<select name="date_fin_pref" id="date_fin_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
</span>
<span class="filter-line">
<span class="filter-title" for="etat">Etat</span>
<input checked type="checkbox" name="etat_present" id="etat_present" class="rbtn present" value="present">
<input checked type="checkbox" name="etat_retard" id="etat_retard" class="rbtn retard" value="retard">
<input checked type="checkbox" name="etat_absent" id="etat_absent" class="rbtn absent" value="absent">
</span>
<span class="filter-line">
<span class="filter-title" for="moduleimpl_id">Module</span>
<select id="moduleimpl_id">
<option value="">Pas de filtre</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="est_just">Est Justifiée</span>
<select id="est_just">
<option value="">Pas de filtre</option>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="etud">Rechercher dans les étudiants</span>
<input type="text" name="etud" id="etud" placeholder="Anne Onymous" >
</span>
</div>
`;
const span = document.createElement('span');
span.innerHTML = html
html = span.firstElementChild
const filterHead = html.querySelector('.filter-head');
filterHead.innerHTML = ""
let cols = ["etudid", "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"];
cols.forEach((k) => {
const label = document.createElement('label')
label.classList.add('f-label')
const s = document.createElement('span');
s.textContent = columnTranslator(k);
const input = document.createElement('input');
input.classList.add('chk')
input.type = "checkbox"
input.name = k
input.id = k;
input.checked = filterAssiduites.columns.includes(k)
label.appendChild(s)
label.appendChild(input)
filterHead.appendChild(label)
})
const sl = html.querySelector('.filter-line #moduleimpl_id');
let opts = []
Object.keys(moduleimpls).forEach((k) => {
const opt = document.createElement('option');
opt.value = k == null ? "null" : k;
opt.textContent = moduleimpls[k];
opts.push(opt);
})
opts = opts.sort((a, b) => {
return a.value < b.value
})
sl.append(...opts);
// Mise à jour des filtres
Object.keys(filterAssiduites.filters).forEach((key) => {
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
if (key.indexOf('date') != -1) {
l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref;
l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm");
} else if (key.indexOf('etat') != -1) {
l.querySelectorAll('input').forEach((e) => {
e.checked = filterAssiduites.filters[key].includes(e.value)
})
} else if (key.indexOf("module") != -1) {
l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key];
} else if (key.indexOf("est_just") != -1) {
l.querySelector('#est_just').value = filterAssiduites.filters[key];
} else if (key == "etud") {
l.querySelector('#etud').value = filterAssiduites.filters["etud"];
}
})
openPromptModal("Filtrage des assiduités", html, () => {
const columns = [...document.querySelectorAll('.chk')]
.map((el) => { if (el.checked) return el.id })
.filter((el) => el)
filterAssiduites.columns = columns
filterAssiduites.filters = {}
//reste des filtres
const lines = [...document.querySelectorAll('.filter-line')];
lines.forEach((l) => {
const key = l.querySelector('.filter-title').getAttribute('for');
if (key.indexOf('date') != -1) {
const pref = l.querySelector(`#${key}_pref`).value;
const time = l.querySelector(`#${key}_time`).value;
if (l.querySelector(`#${key}_time`).value != "") {
filterAssiduites.filters[key] = {
pref: pref,
time: new Date(Date.removeUTC(time))
}
}
} else if (key.indexOf('etat') != -1) {
filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
} else if (key.indexOf("module") != -1) {
filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value;
} else if (key.indexOf("est_just") != -1) {
filterAssiduites.filters[key] = l.querySelector('#est_just').value;
} else if (key == "etud") {
filterAssiduites.filters["etud"] = l.querySelector('#etud').value;
}
})
getAssi(assiduiteCallBack)
}, () => { }, "var(--color-primary)");
}
function downloadAssi() {
getAssi((d) => { toCSV(d, filterAssiduites) })
}
function getAssi(action) {
try { getAllAssiduitesFromEtud(etudid, action, true, true, assi_limit_annee) } catch (_) { }
}
</script>

View File

@ -89,8 +89,7 @@
} }
function timelineMainEvent(event, callback) { function timelineMainEvent(event) {
const func_call = callback ? callback : () => { };
const startX = (event.clientX || event.changedTouches[0].clientX); const startX = (event.clientX || event.changedTouches[0].clientX);
@ -152,7 +151,6 @@
updatePeriodTimeLabel(); updatePeriodTimeLabel();
}; };
const mouseUp = () => { const mouseUp = () => {
generateAllEtudRow();
snapHandlesToQuarters(); snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove); timelineContainer.removeEventListener("mousemove", onMouseMove);
func_call(); func_call();
@ -172,9 +170,12 @@
} }
} }
let func_call = () => { };
function setupTimeLine(callback) { function setupTimeLine(callback) {
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e, callback) }); func_call = callback;
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e, callback) }); timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) });
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) });
} }
function adjustPeriodPosition(newLeft, newWidth) { function adjustPeriodPosition(newLeft, newWidth) {
@ -230,8 +231,8 @@
periodTimeLine.style.width = `${widthPercentage}%`; periodTimeLine.style.width = `${widthPercentage}%`;
snapHandlesToQuarters(); snapHandlesToQuarters();
generateAllEtudRow();
updatePeriodTimeLabel() updatePeriodTimeLabel()
func_call();
} }
function snapHandlesToQuarters() { function snapHandlesToQuarters() {
@ -270,7 +271,6 @@
if (heure_deb != '' && heure_fin != '') { if (heure_deb != '' && heure_fin != '') {
heure_deb = fromTime(heure_deb); heure_deb = fromTime(heure_deb);
heure_fin = fromTime(heure_fin); heure_fin = fromTime(heure_fin);
console.warn(heure_deb, heure_fin)
setPeriodValues(heure_deb, heure_fin) setPeriodValues(heure_deb, heure_fin)
} }
{% endif %} {% endif %}

View File

@ -1,3 +1,4 @@
{# Base de toutes les pages ScoDoc #}
{% block doc -%} {% block doc -%}
<!DOCTYPE html> <!DOCTYPE html>
<html{% block html_attribs %}{% endblock html_attribs %}> <html{% block html_attribs %}{% endblock html_attribs %}>
@ -25,7 +26,10 @@
{% block scripts %} {% block scripts %}
<script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script> <script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script>
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap/js/bootstrap.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/bootstrap/js/bootstrap.min.js"></script>
<script>
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
</script>
{%- endblock scripts %} {%- endblock scripts %}
{%- endblock body %} {%- endblock body %}
</body> </body>

Some files were not shown because too many files have changed in this diff Show More