Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
131 changed files with 4762 additions and 3729 deletions
Showing only changes of commit 83c6ec44c8 - Show all commits

View File

@ -3,9 +3,11 @@
from flask_json import as_json
from flask import Blueprint
from flask import request, g
from flask_login import current_user
from app import db
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoException
from app.scodoc.sco_permissions import Permission
api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__)
@ -48,13 +50,21 @@ def requested_format(default_format="json", allowed_formats=None):
@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]"
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
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)
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,
message=f"{model_cls.__name__} inexistant(e)",
)
return unique.to_dict(format_api=True)
if restrict is None:
return unique.to_dict(format_api=True)
return unique.to_dict(format_api=True, restrict=restrict)
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)
)
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:
data = [etud.to_dict_short() for etud in etuds]
return data
@ -138,8 +139,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
404,
message="étudiant inconnu",
)
return etud.to_dict_api()
restrict = not current_user.has_permission(Permission.ViewEtudData)
return etud.to_dict_api(restrict=restrict)
@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(
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>")
@ -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)
# 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")
@ -543,7 +549,8 @@ def etudiant_create(force=False):
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
db.session.refresh(etud)
r = etud.to_dict_api()
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
return r
@ -590,5 +597,6 @@ def etudiant_edit(
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
db.session.refresh(etud)
r = etud.to_dict_api()
restrict = not current_user.has_permission(Permission.ViewEtudData)
r = etud.to_dict_api(restrict=restrict)
return r

View File

@ -67,7 +67,7 @@ def get_evaluation(evaluation_id: int):
@scodoc
@permission_required(Permission.ScoView)
@as_json
def evaluations(moduleimpl_id: int):
def moduleimpl_evaluations(moduleimpl_id: int):
"""
Retourne la liste des évaluations d'un moduleimpl
@ -75,14 +75,8 @@ def evaluations(moduleimpl_id: int):
Exemple de résultat : voir /evaluation
"""
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
return [e.to_dict_api() for e in query]
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
@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_json import as_json
from flask_login import login_required
from flask_login import current_user, login_required
import app
from app import db
@ -360,7 +360,8 @@ def formsemestre_etudiants(
inscriptions = formsemestre.inscriptions
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:
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
# 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"
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
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(
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_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_web_bp
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",
"etat": "valide",
"fichier": "archive_id",
"raison": "une raison",
"raison": "une raison", // VIDE si pas le droit
"entry_date": "2022-10-31T08:00+01:00",
"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
@ -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
data_set: list[dict] = []
restrict = not current_user.has_permission(Permission.AbsJustifView)
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)
return data_set
@ -151,7 +157,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
@as_json
@permission_required(Permission.ScoView)
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
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)
# Mise en forme des données et retour JSON
restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = []
for just in justificatifs_query:
data_set.append(_set_sems(just))
data_set.append(_set_sems(just, restrict=restrict))
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
@ -189,7 +199,7 @@ def _set_sems(justi: Justificatif) -> dict:
dict: La représentation de l'assiduité 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é
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)
# Retour des justificatifs en JSON
restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = []
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)
return data_set
@ -294,6 +305,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
404,
message="étudiant inconnu",
)
set_sco_dept(etud.departement.acronym)
# Récupération des justificatifs à créer
create_list: list[object] = request.get_json(force=True)

View File

@ -8,16 +8,14 @@
ScoDoc 9 API : accès aux moduleimpl
"""
from flask import g
from flask_json import as_json
from flask_login import login_required
import app
from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required
from app.models import (
FormSemestre,
ModuleImpl,
)
from app.models import ModuleImpl
from app.scodoc import sco_liste_notes
from app.scodoc.sco_permissions import Permission
@ -62,10 +60,7 @@ def moduleimpl(moduleimpl_id: int):
}
}
"""
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
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)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
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
"""
import datetime
from flask import g, request
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.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 is_valid_password
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_permissions import Permission
from app.scodoc.sco_utils import json_error
from app.scodoc import sco_utils as scu
@ -441,3 +441,63 @@ def role_delete(role_name: str):
db.session.delete(role)
db.session.commit()
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.update(self.res.sem)
etud_etat = self.res.get_etud_etat(etud.id)
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
etud_etat,
self.prefs,
decision_sem=d["semestre"].get("decision"),
d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
etud_etat, self.prefs, etud.id, res=self.res
)
if etud_etat == scu.DEMISSION:
d["demission"] = "(Démission)"

View File

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

View File

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

View File

@ -380,14 +380,24 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ADJ,
] + self.codes
explanation += f" et {self.nb_rcues_under_8} < 8"
else:
self.codes = [
sco_codes.RED,
sco_codes.NAR,
sco_codes.PAS1NCI,
sco_codes.ADJ,
sco_codes.PASD, # voir #488 (discutable, conventions locales)
] + self.codes
else: # autres cas: non admis, non passage, non dem, pas la moitié des rcue:
if formsemestre.semestre_id % 2 and self.formsemestre_pair is None:
# Si jury sur un seul semestre impair, ne propose pas redoublement
# et efface décision éventuellement existante
codes = [None]
else:
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
} 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)
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 paire, S_pair est l'origine, et S_impair l'antérieur
@ -524,9 +534,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
return None, None
if formsemestre.semestre_id % 2:
idx_autre = formsemestre.semestre_id + 1
idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
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:
autre_formsemestre = None
@ -610,6 +620,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def next_semestre_ids(self, code: str) -> set[int]:
"""Les indices des semestres dans lequels l'étudiant est autorisé
à 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
# 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,
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")
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:
"""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 le code est None, efface le code déjà enregistré.
Si mark_recorded est vrai, positionne self.recorded
"""
if self.inscription_etat != scu.INSCRIT:
@ -746,7 +760,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
return True
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:
return
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_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_target": url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
),

View File

@ -447,7 +447,7 @@ def jury_but_semestriel(
<div class="nom_etud">{etud.nomprenom}</div>
</div>
<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>
</div>
</div>

View File

@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
raise ScoValueError(
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
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>
<p>Il faut <a href="{
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.file import MultipleFileField
from wtforms import (
BooleanField,
SelectField,
StringField,
SubmitField,
@ -136,6 +137,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
"Module",
choices={}, # will be populated dynamically
)
est_just = BooleanField("Justifiée")
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):

View File

@ -34,52 +34,11 @@ import re
from flask_wtf import FlaskForm
from wtforms import DecimalField, SubmitField, ValidationError
from wtforms.fields.simple import StringField
from wtforms.validators import Optional
from wtforms.validators import Optional, Length
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):
"""Le tick_time doit être entre 0 et 60 minutes"""
if field.data < 1 or field.data > 59:
@ -118,14 +77,36 @@ def check_ics_regexp(form, field):
class ConfigAssiduitesForm(FlaskForm):
"Formulaire paramétrage Module Assiduité"
assi_morning_time = TimeField(
"Début de la journée"
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm
assi_lunch_time = TimeField(
"Heure de midi (date pivot entre matin et après-midi)"
) # TODO
assi_afternoon_time = TimeField("Fin de la journée") # TODO
assi_morning_time = StringField(
"Début de la journée",
default="",
validators=[Length(max=5)],
render_kw={
"class": "timepicker",
"size": 5,
"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(
"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)
"""
from datetime import datetime
from flask_login import current_user
from flask_sqlalchemy.query import Query
from sqlalchemy.exc import DataError
@ -88,8 +89,10 @@ class Assiduite(ScoDocModel):
lazy="select",
)
def to_dict(self, format_api=True) -> dict:
"""Retourne la représentation json de l'assiduité"""
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
"""Retourne la représentation json de l'assiduité
restrict n'est pas utilisé ici.
"""
etat = self.etat
user: User | None = None
if format_api:
@ -252,43 +255,19 @@ class Assiduite(ScoDocModel):
def set_moduleimpl(self, moduleimpl_id: int | str):
"""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)
- <str> ("autre" ou "<id>")
- None (pas de moduleimpl_id)
- "" (pas de moduleimpl_id)
Si la valeur est "autre" il faut:
- mettre à None assiduité.moduleimpl_id
- 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é
"""
moduleimpl: ModuleImpl = None
try:
# 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")
if moduleimpl_id == "autre":
# Configuration de external_data pour Module Autre
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
# Sinon on met à jour external_data["module"] à "autre"
@ -302,6 +281,29 @@ class Assiduite(ScoDocModel):
self.moduleimpl_id = None
# 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):
"Supprime l'assiduité. Log et commit."
@ -331,7 +333,7 @@ class Assiduite(ScoDocModel):
return get_formsemestre_from_data(self.to_dict())
def get_module(self, traduire: bool = False) -> int | str:
"TODO"
"TODO documenter"
if self.moduleimpl_id is not None:
if traduire:
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
@ -360,8 +362,12 @@ class Assiduite(ScoDocModel):
return f"saisie le {date} {utilisateur}"
def _check_force_module(self, moduleimpl: ModuleImpl) -> bool:
# Vérification si module forcé
def _check_force_module(self):
"""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(
{
"etudid": self.etudid,
@ -369,18 +375,15 @@ class Assiduite(ScoDocModel):
"date_fin": self.date_fin,
}
)
force: bool
if formsemestre:
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
else:
force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id)
formsemestre_id = formsemestre.id if formsemestre else None
# si pas de formsemestre, utilisera les prefs globales du département
dept_id = self.etudiant.dept_id
force = is_assiduites_module_forced(
formsemestre_id=formsemestre_id, dept_id=dept_id
)
if force:
raise ScoValueError("Module non renseigné")
return True
class Justificatif(ScoDocModel):
"""
@ -434,6 +437,14 @@ class Justificatif(ScoDocModel):
etudiant = db.relationship(
"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)
@ -445,20 +456,16 @@ class Justificatif(ScoDocModel):
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
return query.first_or_404()
def to_dict(self, format_api: bool = False) -> dict:
"""transformation de l'objet en dictionnaire sérialisable"""
def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
"""L'objet en dictionnaire sérialisable.
Si restrict, ne donne par la raison et les fichiers et external_data
"""
etat = self.etat
username = self.user_id
user: User = self.user if self.user_id is not None else None
if format_api:
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 = {
"justif_id": self.justif_id,
@ -467,11 +474,13 @@ class Justificatif(ScoDocModel):
"date_debut": self.date_debut,
"date_fin": self.date_fin,
"etat": etat,
"raison": self.raison,
"fichier": self.fichier,
"raison": None if restrict else self.raison,
"fichier": None if restrict else self.fichier,
"entry_date": self.entry_date,
"user_id": username,
"external_data": self.external_data,
"user_id": None if user is None else user.id, # l'uid
"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
@ -618,6 +627,12 @@ def compute_assiduites_justified(
Returns:
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
if justificatifs is None:
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"
)
# Champs "protégés" par ViewEtudData (RGPD)
protected_attrs = {"boursier"}
def __repr__(self):
return (
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:
"url de la fiche étudiant"
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
@ -418,7 +421,7 @@ class Identite(models.ScoDocModel):
return args_dict
def to_dict_short(self) -> dict:
"""Les champs essentiels"""
"""Les champs essentiels (aucune donnée perso protégée)"""
return {
"id": self.id,
"civilite": self.civilite,
@ -433,9 +436,10 @@ class Identite(models.ScoDocModel):
"prenom_etat_civil": self.prenom_etat_civil,
}
def to_dict_scodoc7(self) -> dict:
def to_dict_scodoc7(self, restrict=False) -> dict:
"""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.pop("_sa_instance_state", None)
@ -446,7 +450,7 @@ class Identite(models.ScoDocModel):
e_dict["nomprenom"] = self.nomprenom
adresse = self.adresses.first()
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
def to_dict_bul(self, include_urls=True):
@ -481,7 +485,7 @@ class Identite(models.ScoDocModel):
if include_urls and has_request_context():
# test request context so we can use this func in tests under the flask shell
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)
adresse = self.adresses.first()
@ -490,16 +494,22 @@ class Identite(models.ScoDocModel):
d["id"] = self.id # a été écrasé par l'id de adresse
return d
def to_dict_api(self) -> dict:
"""Représentation dictionnaire pour export API, avec adresses et admission."""
def to_dict_api(self, restrict=False) -> dict:
"""Représentation dictionnaire pour export API, avec adresses et admission.
Si restrict, supprime les infos "personnelles" (boursier)
"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
admission = self.admission
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.pop("departement", None)
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
def inscriptions(self) -> list["FormSemestreInscription"]:
@ -825,12 +835,25 @@ class Adresse(models.ScoDocModel):
)
description = db.Column(db.Text)
def to_dict(self, convert_nulls_to_str=False):
"""Représentation dictionnaire,"""
# Champs "protégés" par ViewEtudData (RGPD)
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.pop("_sa_instance_state", None)
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
@ -885,12 +908,16 @@ class Admission(models.ScoDocModel):
# classement (1..Ngr) par le jury dans le groupe APB
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:
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
return Baccalaureat(self.bac, specialite=self.specialite)
def to_dict(self, no_nulls=False):
"""Représentation dictionnaire,"""
def to_dict(self, no_nulls=False, restrict=False):
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
if no_nulls:
@ -905,6 +932,8 @@ class Admission(models.ScoDocModel):
d[key] = 0
elif isinstance(col_type, sqlalchemy.Boolean):
d[key] = False
if restrict:
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
return d
@classmethod

View File

@ -184,7 +184,7 @@ class Evaluation(db.Model):
# ScoDoc7 output_formators
e_dict["evaluation_id"] = self.id
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["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
@ -428,8 +428,8 @@ class Evaluation(db.Model):
def get_ue_poids_str(self) -> str:
"""string describing poids, for excel cells and pdfs
Note: si les poids ne sont pas initialisés (poids par défaut),
ils ne sont pas affichés.
Note: les poids nuls ou non initialisés (poids par défaut),
ne sont pas affichés.
"""
# 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
@ -440,7 +440,7 @@ class Evaluation(db.Model):
for p in sorted(
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:
duration = data["date_fin"] - data["date_debut"]
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
raise ScoValueError("Heures de l'évaluation incohérentes !")
# # --- heures
# heure_debut = data.get("heure_debut", None)
# 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)
raise ScoValueError(
"Heures de l'évaluation incohérentes !",
dest_url="javascript:history.back();",
)
def heure_to_time(heure: str) -> datetime.time:

View File

@ -187,7 +187,7 @@ class FormSemestre(db.Model):
def get_formsemestre(
cls, formsemestre_id: int | str, dept_id: int = None
) -> "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):
try:
formsemestre_id = int(formsemestre_id)

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls
"""
import pandas as pd
from flask import abort, g
from flask_sqlalchemy.query import Query
from app import db
@ -82,6 +83,23 @@ class ModuleImpl(db.Model):
df_cache.EvaluationsPoidsCache.set(self.id, 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):
"""Invalide poids cachés"""
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">'
% (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":
lem.append(
'<input type="text" name="%s" id="%s" size="%d" %s'

View File

@ -145,7 +145,9 @@ def sco_header(
etudid=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
if etudid is not None:
@ -189,7 +191,12 @@ def sco_header(
# jQuery UI
# can modify loaded theme here
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:
# It may be necessary to add an API key:
@ -219,19 +226,26 @@ def sco_header(
# jQuery
H.append(
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
f"""
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
"""
)
# qTip
if init_qtip:
H.append(
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(
f"""<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}/js/scodoc.js"></script>"""
f"""<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:
H.append(

View File

@ -32,12 +32,68 @@ from flask import render_template, url_for
from flask import g, request
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
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
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():
"partie commune à toutes les sidebar"
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]
params.update(etud)
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
H.append(
@ -129,13 +185,17 @@ def sidebar(etudid: int = None):
)
H.append("<ul>")
if current_user.has_permission(Permission.AbsChange):
# essaie de conserver le semestre actuellement en vue
cur_formsemestre_id = retreive_formsemestre_from_request()
H.append(
f"""
<li><a href="{ url_for('assiduites.ajout_assiduite_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Ajouter</a></li>
<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>
"""
)

View File

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

View File

@ -34,7 +34,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence.
import datetime
from typing import Optional
from flask import current_app, g, url_for
from flask import g, url_for
from flask_mail import Message
from app import db
@ -42,6 +42,7 @@ from app import email
from app import log
from app.auth.models import User
from app.models.absences import AbsenceNotification
from app.models.etudiants import Identite
from app.models.events import Scolog
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
@ -108,7 +109,6 @@ def do_abs_notify(
return # abort
# 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")
destinations_filtered = []
for email_addr in destinations:
@ -175,9 +175,15 @@ def abs_notify_get_destinations(
if prefs["abs_notify_email"]:
destinations.append(prefs["abs_notify_email"])
if prefs["abs_notify_etud"]:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
if etud["email_default"]:
destinations.append(etud["email_default"])
etud = Identite.get_etud(etudid)
adresse = etud.adresses.first()
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
# à cette date
@ -271,7 +277,7 @@ def abs_notification_message(
values["nbabsjust"] = nbabsjust
values["nbabsnonjust"] = nbabs - nbabsjust
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"]

View File

@ -62,7 +62,7 @@ def can_edit_etud_archive(authuser):
def etud_list_archives_html(etud: Identite):
"""HTML snippet listing archives"""
"""HTML snippet listing archives."""
can_edit = can_edit_etud_archive(current_user)
etud_archive_id = etud.id
L = []
@ -177,7 +177,7 @@ def etud_upload_file_form(etudid):
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
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:
data = tf[2]["datafile"].read()
@ -188,7 +188,7 @@ def etud_upload_file_form(etudid):
etud_archive_id, data, filename, description=descr
)
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="",
cancel_url=url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
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"])
flash("Archive supprimée")
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:
"""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:
@ -39,7 +47,7 @@ class Trace:
continue
entry_date: datetime = is_iso_formated(csv[1], 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]
if os.path.isfile(self.path):
@ -84,7 +92,14 @@ class Trace:
self, fnames: list[str] = None
) -> dict[str, list[datetime, datetime, str]]:
"""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:
return self.content
@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver):
filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
trace: Trace = Trace(archive_id)
traced = trace.get_trace(filenames)
return [(key, value[2]) for key, value in traced.items()]
return [(key, value[2]) for key, value in traced.items() if value is not None]
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 app import log, db
from app import log, db, set_sco_dept
import app.scodoc.sco_utils as scu
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
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é
et si elle est justifié
"""
keys: dict[EtatAssiduite, str] = {
keys: dict[scu.EtatAssiduite, str] = {
scu.EtatAssiduite.ABSENT: "absent",
scu.EtatAssiduite.RETARD: "retard",
scu.EtatAssiduite.PRESENT: "present",
@ -349,6 +349,11 @@ def get_assiduites_stats(
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
) -> dict[str, int | float]:
"""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:
deb, fin = None, None
@ -390,17 +395,18 @@ def get_assiduites_stats(
# Récupération des états
etats: list[str] = (
filtered["etat"].split(",")
if "etat" in filtered
else ["absent", "present", "retard"]
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
)
# ê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
count: dict = calculator.to_dict(only_total=False)
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":
output[etat] = count[etat]
output[etat]["justifie"] = count[etat + "_just"]
@ -452,8 +458,6 @@ def filter_by_date(
if date_fin is None:
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:
return collection.filter(
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
@ -560,15 +564,19 @@ def get_all_justified(
return after
def create_absence(
def create_absence_billet(
date_debut: datetime,
date_fin: datetime,
etudid: int,
description: str = None,
est_just: bool = False,
) -> 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()
assiduite_unique: Assiduite = Assiduite.create_assiduite(
etud=etud,
@ -650,8 +658,7 @@ def get_assiduites_count_in_interval(
"""
date_debut_iso = date_debut_iso or date_debut.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}{metrique}_assiduites"
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key)
if not r or moduleimpl_id is not None:
@ -668,26 +675,27 @@ def get_assiduites_count_in_interval(
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites)
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:
ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans:
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):
"""Invalidate (clear) cached counts"""
date_debut = sem["date_debut_iso"]
date_fin = sem["date_fin_iso"]
for met in scu.AssiduitesMetrics.TAG:
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
sco_cache.AbsSemEtudCache.delete(key)
key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites"
sco_cache.AbsSemEtudCache.delete(key)
# Non utilisé
def invalidate_assiduites_count_sem(sem: dict):
"""Invalidate (clear) cached abs counts for all the students of this semestre"""
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"]
invalidate_assiduites_etud_date(etudid, date_debut)
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 app import log, ScoValueError
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
@ -318,14 +319,34 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
return pdfdoc, filename
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf"""
def get_filigranne(
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:
return "Démission"
elif etud_etat == codes_cursus.DEF:
if etud_etat == codes_cursus.DEF:
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"
]:
return prefs["bul_temporary_txt"]
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"
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 import sco_cache, sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formations
from app.scodoc.codes_cursus import (
CMP,
ADC,

View File

@ -134,10 +134,10 @@ def table_debouche_etudids(etudids, keep_numeric=True):
"nom": etud["nom"],
"prenom": etud["prenom"],
"_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(
"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"]),
# 'debouche' : etud['debouche'],

View File

@ -177,12 +177,26 @@ def formsemestre_edt_dict(
TODO: spécifier intervalle de dates start et end
"""
t0 = time.time()
group_ids_set = set(group_ids) if group_ids else set()
try:
events_scodoc, _ = load_and_convert_ics(formsemestre)
except ScoValueError as exc:
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"
title="promotion complète" alt="promotion"/>"""
abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg"
@ -211,7 +225,6 @@ def formsemestre_edt_dict(
url_for(
"assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
heure_deb=event["heure_deb"],
heure_fin=event["heure_fin"],
@ -271,17 +284,17 @@ def formsemestre_edt_dict(
"start": event["start"],
"end": event["end"],
"backgroundColor": event["group_bg_color"],
"raw": event["raw"],
# Infos brutes pour usage API éventuel
"edt_ens_ids": event["edt_ens_ids"],
"ens_user_names": ens_user_names,
"group_id": group.id if group else None,
"group_edt_id": event["edt_group"],
"moduleimpl_id": modimpl.id if modimpl else None,
"UID": event["UID"], # icalendar event UID
}
events_cal.append(d)
log(
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
)
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]]:
"""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
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é
- "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
edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
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_pattern = get_ics_uid_pattern()
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
# --- Groupes
group_colors = {
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
for i, group_name in enumerate(edt2group)
}
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)
# ---
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 = 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
if edt_ics_mod_pattern:
edt_module = extract_event_edt_id(
@ -405,6 +417,34 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
else:
modimpl = False
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
users: list[User] = []
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(),
"start": event.decoded("dtstart").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)

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]
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["_prenom_target"] = tgt
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_str"] = {True: "oui", False: "non"}[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["_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["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
else:

View File

@ -692,7 +692,7 @@ class EtapeBilan:
@staticmethod
def link_etu(etudid, nom):
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,
)

View File

@ -64,7 +64,7 @@ def format_etud_ident(etud: dict):
Note: par rapport à Identite.to_dict_bul(),
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"])
if "nom_usuel" in etud:
@ -98,10 +98,6 @@ def format_etud_ident(etud: dict):
etud["ne"] = "e"
else: # 'X'
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):
@ -117,36 +113,6 @@ def _format_etat_civil(etud: dict) -> str:
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):
"laisse le pays seulement si != FRANCE"
if s.upper() != "FRANCE":
@ -283,14 +249,14 @@ def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True)
listh.append(
f"""Autre étudiant: <a href="{
url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=e["etudid"]
)}">{e['nom']} {e['prenom']}</a>"""
)
if etudid:
OK = "retour à la fiche étudiant"
dest_endpoint = "scolar.ficheEtud"
dest_endpoint = "scolar.fiche_etud"
parameters = {"etudid": etudid}
else:
if "tf_submitted" in args:
@ -619,7 +585,7 @@ def create_etud(cnx, args: dict = None):
etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
fill_etuds_info([etud_dict])
etud_dict["url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
ScolarNews.add(
typ=ScolarNews.NEWS_INSCR,
@ -724,19 +690,28 @@ def get_etablissements():
def get_lycee_infos(codelycee):
E = get_etablissements()
return E.get(codelycee, None)
etablissements = get_etablissements()
return etablissements.get(codelycee, None)
def format_lycee_from_code(codelycee):
def format_lycee_from_code(codelycee: str) -> str:
"Description lycee à partir du code"
E = get_etablissements()
if codelycee in E:
e = E[codelycee]
etablissements = get_etablissements()
if codelycee in etablissements:
e = etablissements[codelycee]
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:
return "%s (établissement inconnu)" % codelycee
return nomlycee
def etud_add_lycee_infos(etud):
@ -821,36 +796,6 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
# nettoyage champs souvent vides
etud["codepostallycee"] = etud.get("codepostallycee", "") 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:

View File

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

View File

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

View File

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

View File

@ -145,7 +145,7 @@ def search_etud_in_dept(expnom=""):
if "dest_url" in vals:
endpoint = vals["dest_url"]
else:
endpoint = "scolar.ficheEtud"
endpoint = "scolar.fiche_etud"
if "parameters_keys" in vals:
for key in vals["parameters_keys"].split(","):
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)
H = [
"""<div class="table_etud_in_accessible_depts">""",
"""<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom,
f"""<div class="table_etud_in_accessible_depts">
<h3>Recherche multi-département de "<tt>{expnom}</tt>"</h3>
""",
]
for etuds in result:
if etuds:
@ -337,9 +338,9 @@ def table_etud_in_accessible_depts(expnom=None):
# H.append('<h3>Département %s</h3>' % DeptId)
for e in etuds:
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(
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) }">
inscrire à un autre semestre</a>"
</p>
<h3><a href="{ url_for('scolar.ficheEtud',
<h3><a href="{ url_for('scolar.fiche_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}" 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"]
formsemestre_ext_create(etudid, tf[2])
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(
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>"""
)
return "\n".join(H) + footer
@ -440,7 +440,7 @@ def formsemestre_inscription_with_modules(
dans le semestre {formsemestre.titre_mois()}
</p>
<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>
</li>
<li><a href="{url_for(
@ -501,7 +501,7 @@ def formsemestre_inscription_with_modules(
method="formsemestre_inscription_with_modules",
)
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:
# formulaire choix groupe
@ -656,7 +656,7 @@ function chkbx_select(field_id, state) {
return "\n".join(H) + "\n" + tf[1] + footer
elif tf[0] == -1:
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:
# Inscriptions aux modules choisis
@ -697,7 +697,7 @@ function chkbx_select(field_id, state) {
"""<h3>Aucune modification à effectuer</h3>
<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
@ -755,7 +755,7 @@ function chkbx_select(field_id, state) {
etudid,
modulesimpls_ainscrire,
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
@ -820,7 +820,7 @@ def do_moduleimpl_incription_options(
<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),
html_sco_header.sco_footer(),
]
return "\n".join(H)
@ -885,7 +885,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
'<li><a href="%s" class="discretelink">%s</a> : '
% (
url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
),

View File

@ -51,13 +51,14 @@ from app.models import (
NotesNotes,
)
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 (
ScoValueError,
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 htmlutils
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_users
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
import sco_version
@ -109,7 +111,7 @@ def _build_menu_stats(formsemestre_id):
"title": "Lycées d'origine",
"endpoint": "notes.formsemestre_etuds_lycees",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"enabled": current_user.has_permission(Permission.ViewEtudData),
},
{
"title": 'Table "poursuite"',
@ -336,6 +338,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
formsemestre_id, fix_if_missing=True
),
},
"enabled": current_user.has_permission(Permission.ViewEtudData),
},
{
"title": "Vérifier inscriptions multiples",
@ -474,57 +477,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
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)
def formsemestre_page_title(formsemestre_id=None):
"""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="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre=formsemestre.id,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Justificatifs en attente</button></a>
@ -1457,7 +1409,7 @@ def formsemestre_warning_etuds_sans_note(
noms = ", ".join(
[
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>"""
for etud in etuds
]
@ -1519,13 +1471,13 @@ def formsemestre_note_etuds_sans_notes(
a déjà des notes"""
)
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:
noms = "</li><li>".join(
[
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>"""
for etud in etuds
]

View File

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

View File

@ -52,7 +52,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_etud
from app.scodoc.sco_etud import etud_sort_key
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
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
@ -118,6 +118,16 @@ def groups_view(
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">
<!-- Menu choix groupe -->
{form_groups_choice(groups_infos, submit_on_change=True)}
@ -474,15 +484,12 @@ def groups_table(
"""
from app.scodoc import sco_report
# log(
# "enter groups_table %s: %s"
# % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-"))
# )
can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
with_codes = int(with_codes)
with_paiement = int(with_paiement)
with_archives = int(with_archives)
with_annotations = int(with_annotations)
with_bourse = int(with_bourse)
with_paiement = int(with_paiement) and can_view_etud_data
with_archives = int(with_archives) and can_view_etud_data
with_annotations = int(with_annotations) and can_view_etud_data
with_bourse = int(with_bourse) and can_view_etud_data
base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
base_url = (
@ -527,7 +534,8 @@ def groups_table(
if fmt != "html": # ne mentionne l'état que en Excel (style en html)
columns_ids.append("etat")
columns_ids.append("email")
columns_ids.append("emailperso")
if can_view_etud_data:
columns_ids.append("emailperso")
if fmt == "moodlecsv":
columns_ids = ["email", "semestre_groupe"]
@ -561,7 +569,7 @@ def groups_table(
else:
etud["_emailperso_target"] = ""
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_order"] = etud_sort_key(etud)
@ -616,7 +624,7 @@ def groups_table(
+ "+".join(sorted(moodle_groupenames))
)
else:
filename = "etudiants_%s" % groups_infos.groups_filename
filename = f"etudiants_{groups_infos.groups_filename}"
prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id)
tab = GenTable(
@ -664,28 +672,33 @@ def groups_table(
"""
]
if groups_infos.members:
Of = []
menu_options = []
options = {
"with_paiement": "Paiement inscription",
"with_archives": "Fichiers archivés",
"with_annotations": "Annotations",
"with_codes": "Codes",
"with_bourse": "Statut boursier",
"with_codes": "Affiche codes",
}
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):
selected = "selected"
else:
selected = ""
Of.append(
"""<option value="%s" %s>%s</option>"""
% (option, selected, options[option])
menu_options.append(
f"""<option value="{option}" {selected}>{label}</option>"""
)
H.extend(
[
"""<span style="margin-left: 2em;"><select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
"\n".join(Of),
"""<span style="margin-left: 2em;">
<select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
"\n".join(menu_options),
"""</select></span>
<script type="text/javascript">
$(document).ready(function() {
@ -701,6 +714,9 @@ def groups_table(
});
</script>
""",
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
if not can_view_etud_data
else "",
]
)
H.append("</div></form>")
@ -708,41 +724,45 @@ def groups_table(
H.extend(
[
tab.html(),
"<ul>",
'<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d\'appel Excel</a></li>'
% (tab.base_url,),
'<li><a class="stdlink" href="%s&fmt=xls">Table Excel</a></li>'
% (tab.base_url,),
'<li><a class="stdlink" href="%s&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>'
% (tab.base_url,),
"""<li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a>
f"""
<ul>
<li><a class="stdlink" href="{tab.base_url}&fmt=xlsappel">Feuille d'appel Excel</a>
</li>
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
</li>
<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={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>
</li>"""
% groups_infos.formsemestre_id,
</li>""",
]
)
if amail_inst:
H.append(
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses institutionnelles)</a></li>'
% (
",".join(amail_inst),
groups_infos.groups_titles,
len(amail_inst),
)
f"""<li>
<a class="stdlink" href="mailto:?bcc={','.join(amail_inst)
}">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
(via {len(amail_inst)} adresses institutionnelles)</a>
</li>"""
)
if amail_perso:
H.append(
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses personnelles)</a></li>'
% (
",".join(amail_perso),
groups_infos.groups_titles,
len(amail_perso),
if can_view_etud_data:
if amail_perso:
H.append(
f"""<li>
<a class="stdlink" href="mailto:?bcc={','.join(amail_perso)
}">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
(via {len(amail_perso)} adresses personnelles)</a>
</li>"""
)
)
else:
H.append("<li><em>Adresses personnelles non renseignées</em></li>")
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>")
@ -772,6 +792,10 @@ def groups_table(
filename = "liste_%s" % groups_infos.groups_filename
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
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
if not groups_infos.members:
return ""
@ -829,7 +853,9 @@ def groups_table(
etud, groups_infos.formsemestre_id
)
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]
title = "etudiants_%s" % groups_infos.groups_filename
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,
"""<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
% groups_infos.groups_query_args,
f"""<li><a class="stdlink" href="groups_export_annotations?{groups_infos.groups_query_args}">Liste des annotations</a></li>"""
if authuser.has_permission(Permission.ViewEtudData)
else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>""",
"</ul>",
]
)
@ -899,14 +926,19 @@ def tab_absences_html(groups_infos, etat=None):
"""
)
# 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(
f"""<li><a class="stdlink" href="{
url_for('scolar.etudarchive_import_files_form',
scodoc_dept=g.scodoc_dept,
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>")
return "".join(H)

View File

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

View File

@ -29,6 +29,7 @@
"""
from collections import defaultdict
import numpy as np
import pandas as pd
import flask
from flask import url_for, g, request
@ -264,7 +265,7 @@ def _make_table_notes(
if e.moduleimpl_id != modimpl.id:
raise ValueError("invalid evaluations list")
if fmt == "xls":
if fmt == "xls" or fmt == "json":
keep_numeric = True # pas de conversion des notes en strings
else:
keep_numeric = False
@ -279,11 +280,12 @@ def _make_table_notes(
if anonymous_listing:
columns_ids = ["code"] # cols in table
else:
if fmt == "xls" or fmt == "xml":
columns_ids = ["nom", "prenom"]
if fmt in {"xls", "xml", "json"}:
columns_ids = ["etudid", "nom", "prenom"]
else:
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")
titles = {
@ -476,7 +478,7 @@ def _make_table_notes(
if with_emails:
columns_ids += ["email", "emailperso"]
# 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]
if is_apc:
rows_head.append(row_poids)
@ -683,7 +685,7 @@ def _make_table_notes(
def _add_eval_columns(
evaluation: Evaluation,
eval_state,
evals_poids,
evals_poids: pd.DataFrame | None,
ues,
rows,
titles,
@ -833,14 +835,22 @@ def _add_eval_columns(
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"
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 (
"""<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>"
+ "<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>"
)

View File

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

View File

@ -40,6 +40,7 @@ from app.comp.res_compat import NotesTableCompat
from app.models import (
FormSemestre,
Identite,
ModuleImpl,
Partition,
ScolarFormSemestreValidation,
UniteEns,
@ -52,7 +53,6 @@ from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
@ -63,7 +63,9 @@ import app.scodoc.sco_utils as scu
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
* Gestion des inscriptions
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
"""
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
formsemestre_id = M["formsemestre_id"]
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etudids = etudids or []
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
module = modimpl.module
formsemestre = modimpl.formsemestre
# -- check lock
if not sem["etat"]:
if not formsemestre.etat:
raise ScoValueError("opération impossible: semestre verrouille")
header = html_sco_header.sco_header(
page_title="Inscription au module",
@ -90,25 +92,23 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
footer = html_sco_header.sco_footer()
H = [
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
(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".
</p>
<p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas sur le bouton
"Appliquer les modifications".
<p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas
sur le bouton "Appliquer les modifications".
</p>
"""
% (
moduleimpl_id,
mod["titre"] or "(module sans titre)",
mod["code"] or "(module sans code)",
),
""",
]
# Liste des inscrits à ce semestre
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
formsemestre_id
formsemestre.id
)
for ins in inscrits:
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]
inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
in_m = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=M["moduleimpl_id"]
)
in_module = set([x["etudid"] for x in in_m])
in_m = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
in_module = {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:
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(
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"/>
<p></p>
<table><tr>
{ _make_menu(partitions, "Ajouter", "true") }
{ _make_menu(partitions, "Enlever", "false")}
</tr></table>
<p><br></p>
<table class="sortable" id="mi_table">
<div>
{ _make_menu(partitions, "Ajouter", "true") }
{ _make_menu(partitions, "Enlever", "false")}
</div>
<table class="gt_table mi_table">
<thead>
<tr>
<th>Nom</th>
<th class="etud">Nom</th>
"""
)
for partition in partitions:
if partition["partition_name"]:
H.append("<th>%s</th>" % partition["partition_name"])
H.append("</tr>")
H.append(f"<th>{partition['partition_name']}</th>")
H.append("</tr></thead><tbody>")
for ins in inscrits:
etud = ins["etud"]
@ -178,24 +181,20 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
else:
checked = ""
H.append(
"""<tr><td><input type="checkbox" name="etuds:list" value="%s" %s>"""
% (etud["etudid"], checked)
f"""<tr><td class="etud"><input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>"""
)
H.append(
"""<a class="discretelink etudinfo" href="%s" id="%s">%s</a>"""
% (
f"""<a class="discretelink etudinfo" href="{
url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
),
etud["etudid"],
etud["nomprenom"],
)
)
}" id="{etud['etudid']}">{etud['nomprenom']}</a>"""
)
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:
if partition["partition_name"]:
gr_name = ""
@ -205,11 +204,11 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
break
# gr_name == '' si etud non inscrit dans un groupe de cette partition
H.append(f"<td>{gr_name}</td>")
H.append("""</table></form>""")
H.append("""</tbody></table></form>""")
else: # SUBMISSION
# inscrit a ce module tous les etuds selectionnes
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre_id, etuds, reset=True
moduleimpl_id, formsemestre.id, etudids, reset=True
)
return flask.redirect(
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:
"""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
for partition in partitions:
if partition["partition_name"] != None:
if partition["partition_name"] is not None:
p_idx += 1
for group in sco_groups.get_partition_groups(partition):
items.append(
@ -240,9 +239,9 @@ def _make_menu(partitions: list[dict], title="", check="true") -> str:
}
)
return (
'<td class="inscr_addremove_menu">'
'<div class="inscr_addremove_menu">'
+ 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"]]:
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
H.append(
f"""<li class="etud"><a class="discretelink" href="{
f"""<li class="etud"><a class="discretelink etudinfo"
id="{info['etudid']}"
href="{
url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
)
@ -543,7 +544,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
H.append(
f"""<tr><td><a class="discretelink etudinfo" id={etud.id}
href="{url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
)}"
@ -695,7 +696,7 @@ def _fmt_etud_set(etudids, max_list_size=7) -> str:
[
f"""<a class="discretelink" href="{
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>"""
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
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",
@ -167,6 +168,7 @@ def _ue_coefs_html(coefs_lst) -> str:
{'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
if coef > 0
]
)
+ "</div>"

View File

@ -25,38 +25,37 @@
#
##############################################################################
"""ScoDoc ficheEtud
"""ScoDoc fiche_etud
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
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.models.etudiants import make_etud_args
from app.models import Identite, FormSemestre, ScoDocSiteConfig
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_archives_etud
from app.scodoc import sco_bac
from app.scodoc import codes_cursus
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups
from app.scodoc import sco_cursus
from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos
from app.scodoc import sco_users
from app.scodoc import sco_report
from app.scodoc import sco_etud
from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import (
codes_cursus,
html_sco_header,
htmlutils,
sco_archives_etud,
sco_bac,
sco_cursus,
sco_etud,
sco_groups,
sco_permissions_check,
sco_report,
)
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc.sco_bulletins import etud_descr_situation_semestre
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
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"
authuser = current_user
cnx = ndb.GetDBConnexion()
if etudid:
try: # pour les bookmarks avec d'anciens ids...
etudid = int(etudid)
except ValueError:
raise ScoValueError("id invalide !") from ValueError
# la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar()
g.etudid = etudid
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_
restrict_etud_data = not current_user.has_permission(Permission.ViewEtudData)
try:
etud = Identite.get_etud(etudid)
except Exception as exc:
log(f"fiche_etud: etudid={etudid!r} request.args={request.args!r}")
raise ScoValueError("Étudiant inexistant !") from exc
# la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar()
g.etudid = etudid
info = etud.to_dict_scodoc7(restrict=restrict_etud_data)
if etud.prenom_etat_civil:
info["etat_civil"] = (
"<h3>Etat-civil: "
@ -193,45 +181,26 @@ def ficheEtud(etudid=None):
else:
info["etat_civil"] = ""
info["ScoURL"] = scu.ScoURL()
info["authuser"] = authuser
info["info_naissance"] = info["date_naissance"]
if info["lieu_naissance"]:
info["info_naissance"] += " à " + info["lieu_naissance"]
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"],
)
info["authuser"] = current_user
if restrict_etud_data:
info["info_naissance"] = ""
adresse = None
else:
info["telephones"] = ""
# e-mail:
if info["email_default"]:
info["emaillink"] = ", ".join(
[
'<a class="stdlink" href="mailto:%s">%s</a>' % (m, m)
for m in [etud_["email"], etud_["emailperso"]]
if m
]
)
else:
info["emaillink"] = "<em>(pas d'adresse e-mail)</em>"
info["info_naissance"] = info["date_naissance"]
if info["lieu_naissance"]:
info["info_naissance"] += " à " + info["lieu_naissance"]
if info["dept_naissance"]:
info["info_naissance"] += f" ({info['dept_naissance']})"
adresse = etud.adresses.first()
info.update(_format_adresse(adresse))
info.update(etud.inscription_descr())
info["etudfoto"] = etud.photo_html()
# 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[
"modifadresse"
] = f"""<a class="stdlink" href="{
@ -242,36 +211,33 @@ def ficheEtud(etudid=None):
info["modifadresse"] = ""
# Groupes:
inscription_courante = etud.inscription_courante()
sco_groups.etud_add_group_infos(
info,
info["cursem"]["formsemestre_id"] if info["cursem"] else None,
inscription_courante.formsemestre.id if inscription_courante else None,
only_to_show=True,
)
# Parcours de l'étudiant
if info["sems"]:
info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"]
else:
info["last_formsemestre_id"] = ""
last_formsemestre = None
inscriptions = etud.inscriptions()
info["last_formsemestre_id"] = (
inscriptions[0].formsemestre.id if inscriptions else ""
)
sem_info = {}
for sem in info["sems"]:
formsemestre: FormSemestre = db.session.get(
FormSemestre, sem["formsemestre_id"]
)
if sem["ins"]["etat"] != scu.INSCRIT:
for inscription in inscriptions:
formsemestre = inscription.formsemestre
if inscription.etat != scu.INSCRIT:
descr, _ = etud_descr_situation_semestre(
etudid,
formsemestre,
info["ne"],
etud.e,
show_date_inscr=False,
)
grlink = f"""<span class="fontred">{descr["situation"]}</span>"""
else:
e = {"etudid": etudid}
sco_groups.etud_add_group_infos(
e,
sem["formsemestre_id"],
only_to_show=True,
)
sco_groups.etud_add_group_infos(e, formsemestre.id, only_to_show=True)
grlinks = []
for partition in e["partitions"].values():
@ -289,16 +255,16 @@ def ficheEtud(etudid=None):
)
grlink = ", ".join(grlinks)
# 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:
sem_info[sem["formsemestre_id"]] = (
sem_info[formsemestre.id] = (
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
)
else:
sem_info[sem["formsemestre_id"]] = grlink
sem_info[formsemestre.id] = grlink
if info["sems"]:
Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"])
if inscriptions:
Se = sco_cursus.get_situation_etud_cursus(info, info["last_formsemestre_id"])
info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se,
etudid,
@ -318,20 +284,19 @@ def ficheEtud(etudid=None):
</span>
"""
)
last_formsemestre: FormSemestre = db.session.get(
FormSemestre, info["sems"][0]["formsemestre_id"]
)
last_formsemestre: FormSemestre = inscriptions[0].formsemestre
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
info[
"link_bul_pdf"
] += f"""
<span class="link_bul_pdf">
<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>
</span>
"""
if authuser.has_permission(Permission.EtudInscrit):
if current_user.has_permission(Permission.EtudInscrit):
info[
"link_inscrire_ailleurs"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
@ -348,8 +313,8 @@ def ficheEtud(etudid=None):
info["link_inscrire_ailleurs"] = ""
else:
# non inscrit
l = [f"""<p><b>Étudiant{info["ne"]} non inscrit{info["ne"]}"""]
if authuser.has_permission(Permission.EtudInscrit):
l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
if current_user.has_permission(Permission.EtudInscrit):
l.append(
f"""<a href="{
url_for("notes.formsemestre_inscription_with_modules_form",
@ -362,44 +327,50 @@ def ficheEtud(etudid=None):
info["link_inscrire_ailleurs"] = ""
# Liste des annotations
alist = []
annos = sco_etud.etud_annotations_list(cnx, args={"etudid": etudid})
for a in annos:
if not sco_permissions_check.can_suppress_annotation(a["id"]):
a["dellink"] = ""
else:
a["dellink"] = (
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
% (
etudid,
a["id"],
scu.icontag(
annotations_list = []
annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by(
sa.desc(EtudAnnotation.date)
)
for annot in annotations:
del_link = (
f"""<td class="annodel"><a href="{
url_for("scolar.doSuppressAnnotation",
scodoc_dept=g.scodoc_dept, etudid=etudid, annotation_id=annot.id)}">{
scu.icontag(
"delete_img",
border="0",
alt="suppress",
title="Supprimer cette annotation",
),
)
)
author = sco_users.user_info(a["author"])
alist.append(
f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} :
</span><span class="annoc">{a['comment']}</span></td>{a['dellink']}</tr>
}</a></td>"""
if sco_permissions_check.can_suppress_annotation(annot.id)
else ""
)
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
has_adm_notes = (
info["math"] or info["physique"] or info["anglais"] or info["francais"]
infos_admission = _infos_admission(etud, restrict_etud_data)
has_adm_notes = any(
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
)
has_bac_info = (
info["bac"]
or info["specialite"]
or info["annee_bac"]
or info["rapporteur"]
or info["commentaire"]
or info["classement"]
or info["type_admission"]
has_bac_info = any(
infos_admission[k]
for k in (
"bac_specialite",
"annee_bac",
"rapporteur",
"commentaire",
"classement",
"type_admission",
"rap",
)
)
if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission -->
@ -411,7 +382,7 @@ def ficheEtud(etudid=None):
<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>
<tr>
<td>%(bac)s (%(specialite)s)</td>
<td>%(bac_specialite)s</td>
<td>%(annee_bac)s </td>
<td>%(classement)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>
"""
adm_tmpl += """
<div>Bac %(bac)s (%(specialite)s) obtenu en %(annee_bac)s </div>
<div class="ilycee">%(ilycee)s</div>"""
if info["type_admission"] or info["classement"]:
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
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> """
if info["classement"]:
if infos_admission["classement"]:
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>"
if info["rap"]:
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
else:
adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % info
info["adm_data"] = adm_tmpl % infos_admission
# Fichiers archivés:
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:
@ -455,18 +430,16 @@ def ficheEtud(etudid=None):
if has_debouche:
info[
"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>
<div><form>
<ul class="listdebouches">
%s
{link_add_suivi}
</ul>
</form></div>
</div>""" % (
suivi_readonly,
info["etudid"],
link_add_suivi,
)
</div>"""
else:
info["debouche_html"] = "" # pas de boite "devenir"
#
@ -492,70 +465,92 @@ def ficheEtud(etudid=None):
else:
info["groupes_row"] = ""
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>"""
else:
info["bourse_span"] = ""
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche...
# info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
# XXX dev
info["but_cursus_mkup"] = ""
if info["sems"]:
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
if last_sem.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
info[
"but_cursus_mkup"
] = f"""
<div class="section_but">
{render_template(
"but/cursus_etud.j2",
cursus=but_cursus,
scu=scu,
)}
<div class="link_validation_rcues">
<a href="{url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid,
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>
# Liens vers compétences BUT
if last_formsemestre and last_formsemestre.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
info[
"but_cursus_mkup"
] = f"""
<div class="section_but">
{render_template(
"but/cursus_etud.j2",
cursus=but_cursus,
scu=scu,
)}
<div class="link_validation_rcues">
<a class="stdlink" href="{url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
<div>Compétences BUT</div>
</a>
</div>
"""
</div>
"""
else:
info["but_cursus_mkup"] = ""
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table>
adresse_template = (
""
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>
<h2>%(nomprenom)s (%(inscription)s)</h2>
%(etat_civil)s
<span>%(emaillink)s</span>
<span>%(email_link)s</span>
</td><td class="photocell">
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
</td></tr></table>
<div class="fichesituation">
<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>
"""
+ situation_template
+ """
%(inscriptions_mkup)s
@ -595,8 +590,9 @@ def ficheEtud(etudid=None):
</div>
"""
)
header = html_sco_header.sco_header(
page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
page_title=f"Fiche étudiant {etud.nomprenom}",
cssstyles=[
"libjs/jQuery-tagEditor/jquery.tag-editor.css",
"css/jury_but.css",
@ -614,6 +610,92 @@ def ficheEtud(etudid=None):
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):
"""Menu etudiant (operations sur l'etudiant)"""
authuser = current_user
@ -623,7 +705,7 @@ def menus_etud(etudid):
menuEtud = [
{
"title": etud["nomprenom"],
"endpoint": "scolar.ficheEtud",
"endpoint": "scolar.fiche_etud",
"args": {"etudid": etud["etudid"]},
"enabled": True,
"helpmsg": "Fiche étudiant",
@ -638,7 +720,8 @@ def menus_etud(etudid):
"title": "Changer les données identité/admission",
"endpoint": "scolar.etudident_edit_form",
"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...",
@ -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.
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)
etuds = sco_etud.get_etud_info(filled=True)
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=", ")
etud = Identite.get_etud(etudid)
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()
H = f"""<div class="etud_info_div">
<div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{etud["nomprenom"]}</a></div></div>
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">{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_parcours">{code_cursus}</div>
"""
# Informations sur l'etudiant dans le semestre courant:
sem = None
if formsemestre_id: # un semestre est spécifié par la page
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
elif etud["cursem"]: # le semestre "en cours" pour l'étudiant
sem = etud["cursem"]
if sem:
groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
else:
# le semestre "en cours" pour l'étudiant
inscription_courante = etud.inscription_courante()
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)
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)
if with_photo:
H += '<span class="eid_right">' + photo_html + "</span>"

View File

@ -24,7 +24,7 @@ _SCO_PERMISSIONS = (
(1 << 10, "EditAllNotes", "Modifier toutes les notes"),
(1 << 11, "EditAllEvals", "Modifier toutes les évaluations"),
(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"),
# changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
(1 << 15, "EtudChangeAdr", "Changer les adresses d'étudiants"),
@ -37,7 +37,11 @@ _SCO_PERMISSIONS = (
# aussi pour demissions, diplomes:
(1 << 17, "EtudInscrit", "Inscrire des étudiants"),
# 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 << 20, "EntrepriseChange", "Modifier les entreprises"),
# 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",
),
(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"),
# 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.
)

View File

@ -38,20 +38,19 @@ from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc import sco_assiduites
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
import sco_version
from app.scodoc.gen_tables import GenTable
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' : ... }, {}, ...] }"""
I = {}
I.update(etud) # copie nom, prenom, civilite, ...
infos = {}
infos.update(etud) # copie nom, prenom, civilite, ...
# Now add each semester, starting from the first one
semlist = []
@ -92,25 +91,28 @@ def etud_get_poursuite_info(sem, etud):
for ue in ues: # on parcourt chaque UE
for modimpl in modimpls: # dans chaque UE les modules
if modimpl["module"]["ue_id"] == ue["ue_id"]:
codeModule = modimpl["module"]["code"] or ""
noteModule = scu.fmt_note(
code_module = modimpl["module"]["code"] or ""
note_module = scu.fmt_note(
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:
rangModule = nt.mod_rangs[modimpl["moduleimpl_id"]][
0
][etudid]
rang_module = nt.mod_rangs[
modimpl["moduleimpl_id"]
][0][etudid]
else:
rangModule = ""
modules.append([codeModule, noteModule])
rangs.append(["rang_" + codeModule, rangModule])
rang_module = ""
modules.append([code_module, note_module])
rangs.append(["rang_" + code_module, rang_module])
# Absences
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
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 (
code_semestre_validant(dec["code"])
or code_semestre_attente(dec["code"])
@ -128,9 +130,8 @@ def etud_get_poursuite_info(sem, etud):
("AbsNonJust", nbabs - nbabsjust),
("AbsJust", nbabsjust),
]
d += (
moy_ues + rg_ues + modules + rangs
) # ajout des 2 champs notes des modules et classement dans chaque module
# ajout des 2 champs notes des modules et classement dans chaque module
d += moy_ues + rg_ues + modules + rangs
sem_descr = collections.OrderedDict(d)
if not sem_descr:
sem_descr = collections.OrderedDict(
@ -147,13 +148,14 @@ def etud_get_poursuite_info(sem, etud):
sem_descr["semestre_id"] = sem_id
semlist.append(sem_descr)
I["semlist"] = semlist
return I
infos["semlist"] = semlist
return infos
def _flatten_info(info):
# met la liste des infos semestres "a plat"
# S1_moy, S1_rang, ..., S2_moy, ...
"""met la liste des infos semestres "a plat"
S1_moy, S1_rang, ..., S2_moy, ...
"""
ids = []
for s in info["semlist"]:
for k, v in s.items():
@ -164,7 +166,7 @@ def _flatten_info(info):
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é.
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"):
"""Table avec informations "poursuite" """
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 = []
ids = []
for etud in etuds:
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["_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)
idd = _flatten_info(info)
# 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.
All preferences have a sensible default value, so this
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)
@ -541,18 +542,6 @@ class BasePreferences:
"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",
{
@ -710,11 +699,23 @@ class BasePreferences:
"size": 10,
"title": "Seuil d'alerte des absences",
"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",
"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_url",

View File

@ -144,7 +144,7 @@ def pvjury_table(
"code_nip": e["identite"]["code_nip"],
"nomprenom": e["identite"]["nomprenom"],
"_nomprenom_target": url_for(
"scolar.ficheEtud",
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
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:
etud = Identite.get_etud(etudid)
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>"""
etudids = [etudid]
else:

View File

@ -1017,34 +1017,60 @@ EXP_LIC = re.compile(r"licence", 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"
idx = sem["semestre_id"]
idx = semestre_id
# semestre décalé ?
# les semestres pairs normaux commencent entre janvier et mars
# les impairs normaux entre aout et decembre
d = ""
if idx and idx > 0 and sem["date_debut"]:
mois_debut = int(sem["date_debut"].split("/")[1])
if idx > 0:
if (idx % 2 and mois_debut < 3) or (idx % 2 == 0 and mois_debut >= 8):
d = "d"
if idx == -1:
if short:
idx = "Autre "
else:
idx = sem["titre"] + " "
idx = titre + " "
idx = EXP_LPRO.sub("pro.", idx)
idx = EXP_LIC.sub("Lic.", idx)
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
exemples:
1234A pour un etudiant ayant effectué S1, S2, S3, S4 puis diplome
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
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 }
"""
# Nota: approche plus moderne:
@ -1054,36 +1080,42 @@ def get_code_cursus_etud(etud, prefix="", separator=""):
#
p = []
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:
if nt.get_etud_etat(etud["etudid"]) == "D":
decisions_jury[s["semestre_id"]] = "DEM"
if nt.get_etud_etat(etudid) == "D":
decisions_jury[formsemestre.semestre_id] = "DEM"
else:
dec = nt.get_etud_decision_sem(etud["etudid"])
dec = nt.get_etud_decision_sem(etudid)
if not dec:
decisions_jury[s["semestre_id"]] = ""
decisions_jury[formsemestre.semestre_id] = ""
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
if i == 0:
# Démission
if nt.get_etud_etat(etud["etudid"]) == "D":
if nt.get_etud_etat(etudid) == "D":
p.append(":D")
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:
p.append(":R")
if (
dec
and s["semestre_id"] == nt.parcours.NB_SEM
and formsemestre.semestre_id == nt.parcours.NB_SEM
and code_semestre_validant(dec["code"])
):
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)
codes_etuds = collections.defaultdict(list)
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)
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["_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 = {
"parcours": "Code cursus",
@ -1461,7 +1495,7 @@ def graph_cursus(
else:
modalite = ""
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,
s["mois_debut_ord"],
s["annee_debut"][2:],

View File

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

View File

@ -156,7 +156,7 @@ def trombino_html(groups_infos):
'<a href="%s">%s</a>'
% (
url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
),
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
@ -298,12 +298,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
return None if convert else False
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
"""Ajoute un timecode UTC à la date donnée.
XXX semble faire autre chose... TODO fix this comment
def localize_datetime(date: datetime.datetime) -> datetime.datetime:
"""Transforme une date sans offset en une date avec offset
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
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
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
@ -476,7 +475,7 @@ MONTH_NAMES_ABBREV = (
"Avr ",
"Mai ",
"Juin",
"Jul ",
"Juil ",
"Août",
"Sept",
"Oct ",
@ -1282,6 +1281,27 @@ def format_prenom(s):
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():
"representation du temps courant pour utilisateur"
@ -1610,20 +1630,12 @@ def is_entreprises_enabled():
def is_assiduites_module_forced(
formsemestre_id: int = None, dept_id: int = None
) -> bool:
"""Vrai si préférence "imposer la saisie du module" sur les assiduités est vraie."""
from app.scodoc import sco_preferences
retour: bool
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
return sco_preferences.get_preference(
"forcer_module", formsemestre_id=formsemestre_id, dept_id=dept_id
)
def get_assiduites_time_config(config_type: str) -> str | int:

View File

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

View File

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

View File

@ -1,3 +1,4 @@
// TODO : Supprimer les fonctions non utilisées + optimiser les fonctions utilisées
// <=== CONSTANTS and GLOBALS ===>
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 :
* - Groupe
@ -108,14 +128,16 @@ function validateSelectors(btn) {
return;
}
getAssiduitesFromEtuds(true);
// document.querySelector(".selectors").disabled = true;
// $("#tl_date").datepicker("option", "disabled", true);
generateMassAssiduites();
getAssiduitesFromEtuds(true);
generateAllEtudRow();
// btn.remove();
btn.textContent = "Actualiser";
btn.remove();
// Auto actualisation
$("#tl_date").on("change", updateEtudList);
$("#group_ids_sel").on("change", updateEtudList);
onlyAbs();
};
@ -648,16 +670,15 @@ function updateDate() {
);
openAlertModal("Attention", div, "", "#eec660");
/* BUG TODO MATHIAS
$(dateInput).datepicker("setDate", date_fra); // XXX ??? non définie
dateInput.value = date_fra;
*/
date = lastWorkDay;
dateStr = formatDate(lastWorkDay, {
dateStyle: "full",
timeZone: SCO_TIMEZONE,
}).capitalize();
$(dateInput).datepicker("setDate", date);
$(dateInput).change();
}
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) {
$(input).change(onchange);
}
@ -1262,19 +1338,14 @@ function getAllAssiduitesFromEtud(
.replace("°", courant ? "&courant" : "")
: ""
}`;
//TODO Utiliser async_get au lieu de jquery
$.ajax({
async: true,
type: "GET",
url: url_api,
success: (data, status) => {
if (status === "success") {
assiduites[etudid] = data;
action(data);
}
async_get(
url_api,
(data) => {
assiduites[etudid] = data;
action(data);
},
error: () => {},
});
(_) => {}
);
}
/**
@ -1844,18 +1915,13 @@ function getAllJustificatifsFromEtud(
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
}`;
//TODO Utiliser async_get au lieu de jquery
$.ajax({
async: true,
type: "GET",
url: url_api,
success: (data, status) => {
if (status === "success") {
action(data);
}
async_get(
url_api,
(data) => {
action(data);
},
error: () => {},
});
() => {}
);
}
function deleteJustificatif(justif_id) {

View File

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

View File

@ -1,14 +1,76 @@
from datetime import datetime
from flask import url_for
from flask_sqlalchemy.query import Pagination, Query
from sqlalchemy import desc, literal, union
from flask_login import current_user
from flask_sqlalchemy.query import Query
from sqlalchemy import desc, literal, union, asc
from app import db, g
from app.auth.models import User
from app.models import Assiduite, Identite, Justificatif
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
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):
@ -18,13 +80,15 @@ class ListeAssiJusti(tb.Table):
"""
NB_PAR_PAGE: int = 25
MAX_PAR_PAGE: int = 200
MAX_PAR_PAGE: int = 1000
def __init__(
self,
table_data: "AssiJustifData",
filtre: "AssiFiltre" = None,
options: "AssiDisplayOptions" = None,
no_pagination: bool = False,
titre: str = "",
**kwargs,
) -> None:
"""
@ -41,11 +105,21 @@ class ListeAssiJusti(tb.Table):
# Gestion des options, par défaut un objet Options vide
self.options = options if options is not None else AssiDisplayOptions()
self.no_pagination: bool = no_pagination
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
self.rows: list["RowAssiJusti"] = []
# Titre du tableau, utilisé pour le cache
self.titre = titre
# Instanciation de la classe parent
super().__init__(
row_class=RowAssiJusti,
@ -65,59 +139,93 @@ class ListeAssiJusti(tb.Table):
# Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi
type_obj = self.filtre.type_obj()
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:
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.PRESENT
)
# Non affichage des retards
if not self.options.show_reta:
assiduites_query_etudiants = assiduites_query_etudiants.filter(
Assiduite.etat != EtatAssiduite.RETARD
)
if type_obj in [0, 2]:
justificatifs_query_etudiants = self.table_data.justificatifs_query
# Combinaison des requêtes
query_finale: Query = self.joindre(
query_assiduite=assiduites_query_etudiants,
query_justificatif=justificatifs_query_etudiants,
cle_cache: str = ":".join(
map(
str,
[
self.titre,
type_obj,
self.options.show_pres,
self.options.show_reta,
self.options.show_desc,
self.options.order[0],
self.options.order[1],
],
)
)
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
pagination: Pagination = self.paginer(query_finale)
self.total_pages: int = pagination.pages
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
self.total_pages = pagination.total_pages
# 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.ajouter_colonnes()
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`.
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.
Returns:
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
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.
"""
return query.paginate(
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False
return Pagination(
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):
@ -172,7 +280,7 @@ class ListeAssiJusti(tb.Table):
]
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)
queries.append(query_assiduite)
@ -194,7 +302,7 @@ class ListeAssiJusti(tb.Table):
]
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(
*justificatifs_entities
@ -210,7 +318,7 @@ class ListeAssiJusti(tb.Table):
# Combiner les requêtes avec une union
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
@ -241,30 +349,46 @@ class RowAssiJusti(tb.Row):
# Type d'objet
self._type()
# Date de début
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
# En excel, on export les "vraes dates".
# En excel, on export les "vraies dates".
# 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(
"date_debut",
"Date de début",
self.ligne["date_debut"].strftime("%d/%m/%y")
if multi_days
else self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"),
date_affichees[0],
data={"order": 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
self.add_cell(
"date_fin",
"Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y")
if multi_days
else self.ligne["date_fin"].strftime("à %H:%M"),
date_affichees[1],
raw_content=self.ligne["date_fin"], # Pour excel
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
@ -283,7 +407,11 @@ class RowAssiJusti(tb.Row):
data={"order": self.ligne["entry_date"] or ""},
raw_content=self.ligne["entry_date"],
classes=["small-font"],
column_classes={"entry_date"},
column_classes={
"entry_date",
"external-sort",
"external-type:entry_date",
},
)
def _type(self) -> None:
@ -349,10 +477,21 @@ class RowAssiJusti(tb.Row):
def _optionnelles(self) -> None:
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(
"description",
"Description",
self.ligne["description"] if self.ligne["description"] else "",
description,
)
if self.table.options.show_module:
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
# 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(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
@ -541,6 +684,7 @@ class AssiDisplayOptions:
show_etu: str | bool = True,
show_actions: str | bool = True,
show_module: str | bool = False,
order: tuple[str, str | bool] = None,
):
self.page: int = page
self.nb_ligne_page: int = nb_ligne_page
@ -554,6 +698,10 @@ class AssiDisplayOptions:
self.show_actions = to_bool(show_actions)
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):
"Positionne options booléennes selon arguments"
for k, v in kwargs.items():
@ -565,6 +713,12 @@ class AssiDisplayOptions:
self.nb_ligne_page = min(
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:

View File

@ -129,41 +129,44 @@ class RowAssi(tb.Row):
)
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]] = {
"absent": ["Absences", 0.0, 0.0],
"retard": ["Retards", 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(
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():
compte_etat = scass.get_assiduites_stats(
assiduites=etud.assiduites,
metric=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]
valeur[1] = compte_etat[etat][assi_metric]
if etat != "present":
valeur[2] = compte_etat[etat]["justifie"][assi_metric]
return retour

View File

@ -29,6 +29,15 @@
</em>
</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>
{{ news|safe }}

View File

@ -6,7 +6,6 @@
{% block styles %}
{{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">
{% endblock %}
@ -88,6 +87,13 @@ div.submit > input {
{{ form.modimpl }}
{{ render_field_errors(form, 'modimpl') }}
</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 #}
<div>
<div>{{ form.description.label }}</div>
@ -114,19 +120,7 @@ div.submit > input {
{% block scripts %}
{{ 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/etud_info.js"></script>
<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") }}",
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
dynamic: false,
dropdown: true,
scrollbar: false
});
</script>
{% include "sco_timepicker.j2" %}
{% 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 %}
{{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">
{% endblock %}
@ -18,6 +17,9 @@ form#ajout-justificatif-etud {
form#ajout-justificatif-etud > div {
margin-bottom: 16px;
}
fieldset > div {
margin-bottom: 12px;
}
div.fichiers {
margin-top: 16px;
margin-bottom: 32px;
@ -34,9 +36,21 @@ div.submit {
div.submit > input {
margin-right: 16px;
}
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style>
<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">
@ -73,16 +87,24 @@ div.submit > input {
</div>
{# Raison #}
<div>
<div>{{ form.raison.label }}</div>
{{ form.raison() }}
{{ render_field_errors(form, 'raison') }}
{% if (not justif) or can_view_justif_detail %}
<div>{{ form.raison.label }}</div>
{{ 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 class="fichiers">
{# Liste des fichiers existants #}
{% if justif and nb_files > 0 %}
<div><b>{{nb_files}} fichiers justificatifs déposés
{% 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 %}
</b>
</div>
@ -105,6 +127,7 @@ div.submit > input {
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}
<span class="help">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }}
{# Submit #}
<div class="submit">
{{ form.submit }} {{ form.cancel }}
@ -126,21 +149,9 @@ div.submit > input {
{% block scripts %}
{{ 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/etud_info.js"></script>
<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") }}",
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
dynamic: false,
dropdown: true,
scrollbar: false
});
</script>
{% include "sco_timepicker.j2" %}
<script>
document.addEventListener("DOMContentLoaded", function() {
// Suppression d'un fichier justificatif

View File

@ -1,187 +1,27 @@
{% include "assiduites/widgets/tableau_base.j2" %}
<section class="alerte invisible">
<p>Attention, cet étudiant a trop d'absences</p>
</section>
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
<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">
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<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" %}
{{tableau | safe }}
</section>
<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>
{% endblock app_content %}

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 %}
{% include "assiduites/widgets/tableau_base.j2" %}
<div class="pageContent">
@ -12,9 +79,9 @@
<!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié -->
<h4>Statistiques d'assiduité</h4>
<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>
<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>
<button onclick="stats()">Actualiser</button>
</div>
@ -25,27 +92,7 @@
</section>
<section class="nonvalide">
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
<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" %}
{{tableau | safe }}
</section>
<section class="suppr">
@ -60,36 +107,18 @@
département)</p>
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
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>
{% 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() {
const dd_val = document.getElementById('stats_date_debut').value;
const df_val = document.getElementById('stats_date_fin').value;
@ -111,89 +140,76 @@
}
function getAssiduitesCount(dateDeb, dateFin, query) {
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`;
function getAssiduitesCount(dateDeb, dateFin, action) {
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
return $.ajax({
async: true,
type: "GET",
url: url_api,
success: (data, status) => {
if (status === "success") {
}
async_get(
url_api,
action,
()=>{},
);
}
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) {
//TODO Utiliser Fetch when plutot que jquery
$.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');
}
}
);
getAssiduitesCount(dateDeb, dateFin, showStats);
}
function removeAllAssiduites() {
@ -288,105 +304,12 @@
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_debut').value = assi_date_debut;
loadAll();
stats();
})
</script>
<style>
.stats-values-item {
display: flex;
justify-content: space-evenly;
align-items: center;
flex-direction: column;
}
{% endblock %}
.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" %}
<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 {
.couleurs.print {
@ -593,4 +390,4 @@
</script>
{% endblock pageContent %}
{% endblock app_content %}

View File

@ -1,11 +1,6 @@
{% extends "sco_page.j2" %}
{% 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 %}
{% for err_msg in form.error_messages %}
<div class="wtf-error-messages">
@ -24,7 +19,3 @@
</form>
{% 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 %}
{{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
<style>
div.config-section {
font-weight: bold;
@ -31,8 +32,18 @@ div.config-section {
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></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() {
var inputValue = document.getElementById('test_edt_id').value;
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">
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
{{ wtf.form_field(form.assi_morning_time) }}
{{ wtf.form_field(form.assi_lunch_time) }}
{{ wtf.form_field(form.assi_afternoon_time) }}
{{ wtf.form_field(form.assi_morning_time, class="timepicker") }}
{{ wtf.form_field(form.assi_lunch_time, class="timepicker") }}
{{ wtf.form_field(form.assi_afternoon_time, class="timepicker") }}
{{ wtf.form_field(form.assi_tick_time) }}
</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 %}
<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>
<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>
{{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>
</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 timeMorning = "{{ timeMorning | safe}}";
@ -62,14 +97,5 @@
createColumn();
}
})
</script>
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
{% include "assiduites/widgets/toast.j2" %}
{% endblock scripts %}

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" %}
{{ minitimeline|safe }}
<section id="content">
<div class="no-display">
@ -20,7 +102,7 @@
<div class="infos-button">Groupes&nbsp;: {{grp|safe}}</div>
<div class="infos-button" style="margin-left: 24px;">Date&nbsp;: <span style="margin-left: 8px;"
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()">
</div>
</div>
@ -47,7 +129,6 @@
Faire la saisie
</button>
{% endif %}
<p>Utilisez le bouton "Actualiser" si vous modifier la date ou le(s) groupe(s) sélectionné(s)</p>
<div class="etud_holder">
@ -79,57 +160,6 @@
{% include "assiduites/widgets/prompt.j2" %}
{% include "assiduites/widgets/conflict.j2" %}
<script>
{% if readonly != "false" %}
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>
{% endblock app_content %}

View File

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

View File

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

View File

@ -6,8 +6,8 @@
*/
function getLeftPosition(start) {
const startTime = new Date(start);
const startMins = (startTime.getHours() - 8) * 60 + startTime.getMinutes();
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%";
const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
}
/**
* Ajustement de l'espacement vertical entre les assiduités superposées
@ -76,13 +76,7 @@
const duration = (endTime - startTime) / 1000 / 60;
const percent = (duration / (18 * 60 - 8 * 60)) * 100
if (percent > 100) {
console.log(start, end);
console.log(startTime, endTime)
}
const percent = (duration / (t_end * 60 - t_start * 60)) * 100
return percent + "%";
}
@ -162,6 +156,13 @@
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()
}
@ -246,12 +247,11 @@
*/
splitAssiduiteModal() {
//Préparation du prompt
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend>
<input type="time" id="promptTime" name="appt"
min="08:00" max="18:00" required>`;
const htmlPrompt = `<legend>Entrez l'heure de séparation</legend>
<input type="text" id="promptTime" name="appt"required style="position: relative; z-index: 100000;">`;
const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit");
fieldSet.classList.add("fieldsplit", "timepicker");
fieldSet.innerHTML = htmlPrompt;
//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."
);
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)");
// 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>';
// Ajout des labels d'heure sur la frise chronologique
// TODO permettre la modification des bornes (8 et 18)
for (let i = 8; i <= 18; i++) {
for (let i = t_start; i <= t_end; i++) {
const timeLabel = document.createElement("div");
timeLabel.className = "time-label";
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
@ -460,3 +476,8 @@
}
}
</script>
<style>
.ui-timepicker-container {
z-index: 100000 !important;
}
</style>

View File

@ -270,6 +270,10 @@
-webkit-box-sizing: border-box;
border: 10px solid white;
}
.mini-form {
color: black;
}
</style>
<script>

View File

@ -73,11 +73,6 @@
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
updateJustifyBtn();
}
try {
if (isCalendrier()) {
window.location = `liste_assiduites_etud?etudid=${etudid}&assiduite_id=${assiduité.assiduite_id}`
}
} catch { }
});
//ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité);
@ -138,51 +133,48 @@
*/
function setupAssiduiteBuble(el, assiduite) {
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");
idDiv.className = "assiduite-id";
idDiv.textContent = `${getModuleImpl(assiduite)}`;
bubble.appendChild(idDiv);
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `${getModuleImpl(assiduite)}`;
bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisie le ${formatDateModal(
assiduite.entry_date,
" à "
)}`;
const motifDiv = document.createElement("div");
stateDiv.className = "assiduite-why";
stateDiv.textContent = `Motif: ${assiduite.desc?.capitalize()}`;
bubble.appendChild(motifDiv);
if (assiduite.user_id != null) {
userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}`
}
bubble.appendChild(userIdDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisie le ${formatDateModal(
assiduite.entry_date,
" à "
)}`;
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
bubble.style.top = `${event.clientY + 20}px`;
});
el.addEventListener("mouseout", () => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.classList.remove("is-active");
});
if (assiduite.user_id != null) {
userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}`
}
bubble.appendChild(userIdDiv);
el.appendChild(bubble);
}
function setMiniTick(timelineDate, dayStart, dayDuration) {
@ -199,126 +191,3 @@
}
</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>
{% else %}
<option value=""> Non spécifié </option>
{% endif %}
{% if moduleimpl_id == "autre" %}
<option value="autre" selected> Tout module </option>
{% else %}
<option value="autre"> Tout module </option>
{% endif %}
<option value="autre" {{ 'selected' if moduleimpl_id == 'autre' else '' }}>Autre module (pas dans la liste)</option>

View File

@ -1,6 +1,6 @@
<div>
<div class="sco_box_title">{{ titre }}</div>
<div id="options-tableau">
<div class="options-tableau">
{% if afficher_options != false %}
<input type="checkbox" id="show_pres" name="show_pres"
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>
<br>
{% endif %}
<label for="nb_ligne_page">Nombre de lignes par page : </label>
<input type="number" name="nb_ligne_page" id="nb_ligne_page"
size="4" step="25" min="10" value="{{options.nb_ligne_page}}"
onchange="updateTableau()"
>
<label for="n_page">Page n°</label>
<select name="n_page" id="n_page">
{% for n in range(1,total_pages+1) %}
<option value="{{n}}" {{'selected' if n == options.page else ''}}>{{n}}</option>
<label for="nb_ligne_page">Nombre de lignes par page :</label>
<select name="nb_ligne_page" id="nb_ligne_page" onchange="updateTableau()">
{% for i in [25,50,100,1000] %}
{% if i == options.nb_ligne_page %}
<option selected value="{{i}}">{{i}}</option>
{% else %}
<option value="{{i}}">{{i}}</option>
{% endif %}
{% endfor %}
</select>
<br>
</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>
{{table.html() | safe}}
</div>
<script>
function updateTableau() {
const url = new URL(location.href);
const form = document.getElementById("options-tableau");
const formValues = form.querySelectorAll("*[name]");
const formValues = document.querySelectorAll(".options-tableau *[name]");
formValues.forEach((el) => {
if (el.type == "checkbox") {
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>
<style>
.small-font {
font-size: 9pt;
}
.div-tableau{
display: flex;
flex-direction: column;
align-items: center;
max-width: fit-content;
}
.pagination li{
cursor: pointer;
}
</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 class="info-row">
<span class="info-label">Étudiant{{etud.e}} concerné{{etud.e}}:</span> <span class="etudinfo"
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span>
<div class="info-saisie">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>
<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>
{% if type == "Assiduité" %}
@ -23,27 +46,27 @@
{% else %}
<span class="info-label">État de l'assiduité :</span>
{% endif %}
<b>{{objet.etat}}</b>
<span class="info-etat">{{objet.etat}}</span>
</div>
<div class="info-row">
{% if type == "Justificatif" %}
<div class="info-label">Raison:</div>
{% if objet.raison != None %}
<div class="text">{{objet.raison}}</div>
<span class="info-label">Raison:</span>
{% if can_view_justif_detail %}
<span class="text">{{objet.raison or " "}}</span>
{% else %}
<span class="text unauthorized">(cachée)</span>
{% endif %}
{% else %}
<div class="text">/div>
{% endif %}
{% else %}
<div class="info-label">Description:</div>
<span class="info-label">Description:</span>
{% if objet.description != None %}
<div class="text">{{objet.description}}</div>
<span class="text">{{objet.description}}</span>
{% else %}
<div class="text"></div>
<span class="text"></span>
{% endif %}
{% endif %}
</div>
{% endif %}
</span>
</div>
{# Affichage des justificatifs si assiduité justifiée #}
@ -54,7 +77,8 @@
<span class="text">Oui</span>
<div>
{% for justi in objet.justification.justificatifs %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
<a href="{{url_for('assiduites.tableau_assiduite_actions',
type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
{% endfor %}
</div>
@ -69,13 +93,15 @@
<div class="info-row">
<span class="info-label">Assiduités concernées: </span>
{% if objet.justification.assiduites %}
<div>
<ul>
{% for assi in objet.justification.assiduites %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
<li><a href="{{url_for('assiduites.tableau_assiduite_actions',
type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)
}}" target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
{{assi.date_fin}}</a>
</li>
{% endfor %}
</div>
</ul>
{% else %}
<span class="text">Aucune</span>
{% endif %}
@ -84,27 +110,31 @@
{# Affichage des fichiers des justificatifs #}
{% if type == "Justificatif"%}
<div class="info-row">
<span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li>
<a
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
</li>
{% endfor %}
{% if not objet.justification.fichiers.filenames %}
<li class="fontred">fichiers non visibles</li>
<div class="info-row">
<span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li>
<a
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,
filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
</li>
{% endfor %}
{% if not objet.justification.fichiers.filenames %}
<li class="fontred">fichiers non visibles</li>
{% endif %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</ul>
{% else %}
<span class="text">Aucun</span>
</div>
{% if current_user.has_permission(sco.Permission.AbsChange) %}
<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 %}
</div>
{% endif %}
<div class="info-row">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>
</div>

View File

@ -1,5 +1,7 @@
<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>
Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}}
</div>
@ -39,8 +41,12 @@ Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_de
<option value="modifie">Modifié</option>
</select>
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
{% if current_user.has_permission(sco.Permission.AbsJustifView) %}
<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>

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

View File

@ -1,3 +1,4 @@
{# Base de toutes les pages ScoDoc #}
{% block doc -%}
<!DOCTYPE html>
<html{% block html_attribs %}{% endblock html_attribs %}>
@ -25,7 +26,10 @@
{% block scripts %}
<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 body %}
</body>

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