This commit is contained in:
Emmanuel Viennet 2024-03-01 15:55:03 +01:00
commit 60a97b7baf
27 changed files with 466 additions and 1538 deletions

View File

@ -22,7 +22,6 @@ from app.api import get_model_api_object, tools
from app.decorators import permission_required, scodoc
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
from app.models.assiduites import (
compute_assiduites_justified,
get_formsemestre_from_data,
)
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
@ -310,7 +309,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
errors: list[dict] = []
success: list[dict] = []
justifs: list[Justificatif] = []
# énumération des justificatifs
for i, data in enumerate(create_list):
@ -322,11 +320,9 @@ def justif_create(etudid: int = None, nip=None, ine=None):
errors.append({"indice": i, "message": obj})
else:
success.append({"indice": i, "message": obj})
justifs.append(justi)
justi.justifier_assiduites()
scass.simple_invalidate_cache(data, etud.id)
# Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs
compute_assiduites_justified(etud.etudid, justifs)
return {"errors": errors, "success": success}
@ -495,6 +491,7 @@ def justif_edit(justif_id: int):
return json_error(404, err)
# Mise à jour du justificatif
justificatif_unique.dejustifier_assiduites()
db.session.add(justificatif_unique)
db.session.commit()
@ -511,11 +508,7 @@ def justif_edit(justif_id: int):
retour = {
"couverture": {
"avant": avant_ids,
"apres": compute_assiduites_justified(
justificatif_unique.etudid,
[justificatif_unique],
True,
),
"apres": justificatif_unique.justifier_assiduites(),
}
}
# Invalide le cache
@ -592,14 +585,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
# On invalide le cache
scass.simple_invalidate_cache(justificatif_unique.to_dict())
# On actualise les assiduités justifiées de l'étudiant concerné
justificatif_unique.dejustifier_assiduites()
# On supprime le justificatif
db.session.delete(justificatif_unique)
# On actualise les assiduités justifiées de l'étudiant concerné
compute_assiduites_justified(
justificatif_unique.etudid,
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(),
True,
)
return (200, "OK")
@ -700,7 +689,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
@as_json
@permission_required(Permission.AbsChange)
def justif_remove(justif_id: int = None):
# XXX TODO pas de test unitaire
"""
Supression d'un fichier ou d'une archive
{

View File

@ -393,7 +393,7 @@ class BulletinBUT:
else:
etud_ues_ids = res.etud_ues_ids(etud.id)
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
@ -408,7 +408,7 @@ class BulletinBUT:
}
if self.prefs["bul_show_abs"]:
semestre_infos["absences"] = {
"injustifie": nbabs - nbabsjust,
"injustifie": nbabsnj,
"total": nbabs,
"metrique": {
"H.": "Heure(s)",
@ -525,7 +525,7 @@ class BulletinBUT:
d["demission"] = ""
# --- Absences
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
_, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury
infos, _ = sco_bulletins.etud_descr_situation_semestre(
@ -540,9 +540,9 @@ class BulletinBUT:
d.update(infos)
# --- Rangs
d["rang_nt"] = (
f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
)
d[
"rang_nt"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
d["rang_txt"] = "Rang " + d["rang_nt"]
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))

View File

@ -241,7 +241,7 @@ def bulletin_but_xml_compat(
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
_, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------

View File

@ -574,11 +574,7 @@ class Justificatif(ScoDocModel):
db.session.delete(self)
db.session.commit()
# On actualise les assiduités justifiées de l'étudiant concerné
compute_assiduites_justified(
self.etudid,
Justificatif.query.filter_by(etudid=self.etudid).all(),
True,
)
self.dejustifier_assiduites()
def get_fichiers(self) -> tuple[list[str], int]:
"""Renvoie la liste des noms de fichiers justicatifs
@ -600,6 +596,64 @@ class Justificatif(ScoDocModel):
accessible_filenames.append(filename[0])
return accessible_filenames, len(filenames)
def justifier_assiduites(
self,
) -> list[int]:
"""Justifie les assiduités sur la période de validité du justificatif"""
log(f"justifier_assiduites: {self}")
assiduites_justifiees: list[int] = []
if self.etat != EtatJustificatif.VALIDE:
return []
# On récupère les assiduités de l'étudiant sur la période donnée
assiduites: Query = self.etudiant.assiduites.filter(
Assiduite.date_debut >= self.date_debut,
Assiduite.date_fin <= self.date_fin,
Assiduite.etat != EtatAssiduite.PRESENT,
)
# Pour chaque assiduité, on la justifie
for assi in assiduites:
assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id)
db.session.add(assi)
db.session.commit()
return assiduites_justifiees
def dejustifier_assiduites(self) -> list[int]:
"""
Déjustifie les assiduités sur la période du justificatif
"""
assiduites_dejustifiees: list[int] = []
# On récupère les assiduités de l'étudiant sur la période donnée
assiduites: Query = self.etudiant.assiduites.filter(
Assiduite.date_debut >= self.date_debut,
Assiduite.date_fin <= self.date_fin,
Assiduite.etat != EtatAssiduite.PRESENT,
)
assi: Assiduite
for assi in assiduites:
# On récupère les justificatifs qui justifient l'assiduité `assi`
assi_justifs: list[int] = get_justifs_from_date(
self.etudiant.etudid,
assi.date_debut,
assi.date_fin,
long=False,
valid=True,
)
# Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité
if len(assi_justifs) == 0 or (
len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id
):
assi.est_just = False
assiduites_dejustifiees.append(assi.assiduite_id)
db.session.add(assi)
db.session.commit()
return assiduites_dejustifiees
def is_period_conflicting(
date_debut: datetime,
@ -623,72 +677,6 @@ def is_period_conflicting(
return count > 0
def compute_assiduites_justified(
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
) -> list[int]:
"""
Args:
etudid (int): l'identifiant de l'étudiant
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
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(
etudid=etudid
).all()
# On ne prend que les justificatifs valides
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
# On récupère les assiduités de l'étudiant
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites_justifiees: list[int] = []
for assi in assiduites:
# On ne justifie pas les Présences
if assi.etat == EtatAssiduite.PRESENT:
continue
# On récupère les justificatifs qui justifient l'assiduité `assi`
assi_justificatifs = Justificatif.query.filter(
Justificatif.etudid == assi.etudid,
Justificatif.date_debut <= assi.date_debut,
Justificatif.date_fin >= assi.date_fin,
Justificatif.etat == EtatJustificatif.VALIDE,
).all()
# Si au moins un justificatif possède une période qui couvre l'assiduité
if any(
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
for j in justificatifs + assi_justificatifs
):
# On justifie l'assiduité
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id)
db.session.add(assi)
elif reset:
# Si le paramètre reset est Vrai alors les assiduités non justifiées
# sont remise en "non justifiée"
assi.est_just = False
db.session.add(assi)
# On valide la session
db.session.commit()
# On renvoie la liste des assiduite_id des assiduités justifiées
return assiduites_justifiees
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
"""
get_assiduites_justif Récupération des justificatifs d'une assiduité

View File

@ -875,7 +875,7 @@ class FormSemestre(db.Model):
def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs, nb abs justifiées)
tuple (nb abs non just, nb abs justifiées, nb abs total)
Utilise un cache.
"""
from app.scodoc import sco_assiduites

View File

@ -175,10 +175,9 @@ def sidebar(etudid: int = None):
inscription = etud.inscription_courante()
if inscription:
formsemestre = inscription.formsemestre
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count(
etudid, formsemestre
)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du {
formsemestre.date_debut.strftime("%d/%m/%Y")

View File

@ -67,7 +67,7 @@ def abs_notify(etudid: int, date: str | datetime.datetime):
if not formsemestre:
return # non inscrit a la date, pas de notification
nbabs, nbabsjust = sco_assiduites.get_assiduites_count_in_interval(
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count_in_interval(
etudid,
metrique=scu.translate_assiduites_metric(
sco_preferences.get_preference(

View File

@ -17,7 +17,7 @@ from app.models import (
ModuleImplInscription,
ScoDocSiteConfig,
)
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
from app.models.assiduites import Assiduite, Justificatif
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences
from app.scodoc import sco_cache
@ -111,9 +111,9 @@ class CountCalculator:
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
)
self.non_work_days: list[scu.NonWorkDays] = (
scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
)
self.non_work_days: list[
scu.NonWorkDays
] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
# Sera utilisé pour les assiduités longues (> 1 journée)
self.nb_heures_par_jour = (
@ -661,7 +661,7 @@ def create_absence_billet(
db.session.add(justi)
db.session.commit()
compute_assiduites_justified(etud.id, [justi])
justi.justifier_assiduites()
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites([assiduite_unique])
@ -671,7 +671,7 @@ def create_absence_billet(
# Gestion du cache
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"])
@ -687,17 +687,17 @@ def formsemestre_get_assiduites_count(
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
"""
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval(
etudid,
date_debut=scu.localize_datetime(
datetime.combine(formsemestre.date_debut, time(8, 0))
datetime.combine(formsemestre.date_debut, time(0, 0))
),
date_fin=scu.localize_datetime(
datetime.combine(formsemestre.date_fin, time(18, 0))
datetime.combine(formsemestre.date_fin, time(23, 0))
),
metrique=scu.translate_assiduites_metric(metrique),
moduleimpl_id=moduleimpl_id,
@ -714,12 +714,12 @@ def get_assiduites_count_in_interval(
moduleimpl_id: int = None,
):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées)
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
On peut spécifier les dates comme datetime ou iso.
Utilise un cache.
"""
date_debut_iso = date_debut_iso or date_debut.isoformat()
date_fin_iso = date_fin_iso or date_fin.isoformat()
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key)
@ -744,9 +744,10 @@ def get_assiduites_count_in_interval(
if not ans:
log("warning: get_assiduites_count failed to cache")
nb_abs: dict = r["absent"][metrique]
nb_abs_just: dict = r["absent_just"][metrique]
return (nb_abs, nb_abs_just)
nb_abs: int = r["absent"][metrique]
nb_abs_nj: int = r["absent_non_just"][metrique]
nb_abs_just: int = r["absent_just"][metrique]
return (nb_abs_nj, nb_abs_just, nb_abs)
def invalidate_assiduites_count(etudid: int, sem: dict):

View File

@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
# --- Absences
I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
_, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem)
# --- Decision Jury
infos, dpv = etud_descr_situation_semestre(
@ -471,7 +471,7 @@ def _ue_mod_bulletin(
) # peut etre 'NI'
is_malus = mod["module"]["module_type"] == ModuleType.MALUS
if bul_show_abs_modules:
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
mod_abs = [nbabs, nbabsjust]
mod["mod_abs_txt"] = scu.fmt_abs(mod_abs)
else:

View File

@ -296,7 +296,7 @@ def formsemestre_bulletinetud_published_dict(
# --- Absences
if prefs["bul_show_abs"]:
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
# --- Décision Jury

View File

@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud(
numero=str(mod["numero"]),
titre=quote_xml_attr(mod["titre"]),
abbrev=quote_xml_attr(mod["abbrev"]),
code_apogee=quote_xml_attr(mod["code_apogee"])
code_apogee=quote_xml_attr(mod["code_apogee"]),
# ects=ects ects des modules maintenant inutilisés
)
x_ue.append(x_mod)
@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud(
# --- Absences
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem)
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
# --- Decision Jury
if (

View File

@ -722,8 +722,8 @@ def formsemestre_recap_parcours_table(
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
)
# Absences (nb d'abs non just. dans ce semestre)
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem)
H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""")
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
# UEs
for ue in ues:

View File

@ -105,7 +105,9 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
rangs.append(["rang_" + code_module, rang_module])
# Absences
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count(
etudid, nt.sem
)
# En BUT, prend tout, sinon ne prend que les semestre validés par le jury
if nt.is_apc or (
dec
@ -125,7 +127,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
("date_debut", s["date_debut"]),
("date_fin", s["date_fin"]),
("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])),
("AbsNonJust", nbabs - nbabsjust),
("AbsNonJust", nbabsnj),
("AbsJust", nbabsjust),
]
# ajout des 2 champs notes des modules et classement dans chaque module

View File

@ -620,7 +620,7 @@ class RowRecap(tb.Row):
def add_abs(self):
"Ajoute les colonnes absences"
# Absences (nb d'abs non just. dans ce semestre)
nbabs, nbabsjust = self.table.res.formsemestre.get_abs_count(self.etud.id)
_, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id)
self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs)
self.add_cell(
"nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust
@ -691,9 +691,9 @@ class RowRecap(tb.Row):
self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"])
self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id))
ue_valid_txt = ue_valid_txt_html = (
f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
)
ue_valid_txt = (
ue_valid_txt_html
) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
if self.nb_ues_warning:
ue_valid_txt_html += " " + scu.EMO_WARNING
cell_class = ""
@ -717,9 +717,9 @@ class RowRecap(tb.Row):
# sous-classé par JuryRow pour ajouter les codes
table: TableRecap = self.table
formsemestre: FormSemestre = table.res.formsemestre
table.group_titles["col_ue"] = (
f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
)
table.group_titles[
"col_ue"
] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
col_id = f"moy_ue_{ue.id}"
val = (
ue_status["moy"]

View File

@ -66,7 +66,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
{% endblock styles %}
{% block app_content %}
{% include "assiduites/widgets/tableau_base.j2" %}
<div class="pageContent">
<h2>Bilan de l'assiduité de {{sco.etud.html_link_fiche()|safe}}</span></h2>

View File

@ -1,23 +1,23 @@
{% extends "sco_page.j2" %}
{% block title %}
{{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>
{{ 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>
<script>
{% if readonly != "false" %}
function getPeriodValues(){
function getPeriodValues() {
return [0, 23]
}
{% endif %}
@ -30,9 +30,9 @@
setupDate();
updateDate();
if (!readOnly){
setupTimeLine(()=>{
generateAllEtudRow();
if (!readOnly) {
setupTimeLine(() => {
generateAllEtudRow();
});
}
@ -63,16 +63,16 @@
}
});
}
</script>
</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">
{{ 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 %}
@ -80,7 +80,11 @@
{% include "assiduites/widgets/toast.j2" %}
{{ minitimeline|safe }}
<style>
#moduleimpl_select {
max-width: 200px;
}
</style>
<section id="content">
<div class="no-display">
@ -102,8 +106,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" name="tl_date" id="tl_date" value="{{ date }}"
onchange="updateDate()">
<input type="text" name="tl_date" id="tl_date" value="{{ date }}" onchange="updateDate()">
</div>
</div>
</fieldset>
@ -162,4 +165,4 @@
</section>
{% endblock app_content %}
{% endblock app_content %}

View File

@ -174,7 +174,7 @@
}
window.addEventListener('load', ()=>{
const table_columns = [...document.querySelectorAll('.external-sort')];
const table_columns = [...document.querySelectorAll('th.external-sort')];
table_columns.forEach((e)=>e.addEventListener('click', ()=>{
// récupération de l'ordre "ascending" / "descending"

View File

@ -1,624 +0,0 @@
<ul id="contextMenu" class="context-menu">
<li id="detailOption">Détails</li>
<li id="editOption">Éditer</li>
<li id="deleteOption">Supprimer</li>
</ul>
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/prompt.j2" %}
<script>
const itemsPerPage = 10;
const contextMenu = document.getElementById("contextMenu");
const editOption = document.getElementById("editOption");
const detailOption = document.getElementById("detailOption");
const deleteOption = document.getElementById("deleteOption");
let selectedRow;
document.addEventListener("click", () => {
contextMenu.style.display = "none";
if (contextMenu.childElementCount > 3) {
contextMenu.removeChild(contextMenu.lastElementChild)
}
});
editOption.addEventListener("click", () => {
if (selectedRow) {
const type = selectedRow.getAttribute('type');
const obj_id = selectedRow.getAttribute('obj_id');
if (type == "assiduite") {
editionAssiduites(obj_id);
} else {
editionJustificatifs(obj_id);
}
}
});
detailOption.addEventListener("click", () => {
if (selectedRow) {
const type = selectedRow.getAttribute('type');
const obj_id = selectedRow.getAttribute('obj_id');
if (type == "assiduite") {
detailAssiduites(obj_id);
} else {
detailJustificatifs(obj_id);
}
}
});
deleteOption.addEventListener("click", () => {
if (selectedRow) {
const type = selectedRow.getAttribute('type');
const obj_id = selectedRow.getAttribute('obj_id');
if (type == "assiduite") {
deleteAssiduite(obj_id);
} else {
deleteJustificatif(obj_id);
}
loadAll();
}
});
function filterArray(array, f) {
return array.filter((el) => {
let t = Object.keys(f).every((k) => {
if (k == "etat") {
return f.etat.includes(el.etat.toLowerCase())
};
if (k == "est_just") {
if (f.est_just != "") {
return `${el.est_just}` == f.est_just;
}
}
if (k.indexOf('date') != -1) {
const assi_time = new Date(Date.removeUTC(el[k]));
const filter_time = f[k].time;
switch (f[k].pref) {
case "0":
return assi_time.isSame(filter_time, 'minute');
case "-1":
return assi_time.isBefore(filter_time, 'minutes');
case "1":
return assi_time.isAfter(filter_time, 'minutes');
}
}
if (k == "moduleimpl_id") {
const m = el[k] == undefined || el[k] == null ? "null" : el[k];
if (f.moduleimpl_id != '') {
return m == f.moduleimpl_id;
}
}
if (k == "obj_id") {
const obj_id = el.assiduite_id || el.justif_id;
return f.obj_id.includes(obj_id)
}
if (k == "formsemestre") {
return f.formsemestre === "" || (el.hasOwnProperty("formsemestre") && el.formsemestre.title.replaceAll('-', ' ').indexOf(f.formsemestre) != -1);
}
if (k == "etud") {
const e = getEtudiant(el.etudid);
const str = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`
return f.etud === "" || str.indexOf(f.etud) != -1;
}
return true;
})
return t;
})
}
function generateTableHead(columns, assi = true) {
const table = assi ? "#assiduiteTable" : "#justificatifTable"
const call = assi ? [assiduiteCallBack, true] : [justificatifCallBack, false]
const tr = document.querySelector(`${table} thead tr`);
tr.innerHTML = ""
columns.forEach((c) => {
const th = document.createElement('th');
const div = document.createElement('div');
const span = document.createElement('span');
span.textContent = columnTranslator(c);
const a = document.createElement('a');
a.classList.add('icon', "order");
a.onclick = () => { order(c, call[0], a, call[1]) }
div.appendChild(span)
div.appendChild(a)
th.appendChild(div);
tr.appendChild(th);
})
}
function renderPaginationButtons(array, assi = true) {
const totalPages = Math.ceil(array.length / itemsPerPage);
if (totalPages <= 1) {
if (assi) {
paginationContainerAssiduites.innerHTML = ""
} else {
paginationContainerJustificatifs.innerHTML = ""
}
return;
}
if (assi) {
paginationContainerAssiduites.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'>&lt;</button><select id='paginationAssi'></select><button class='pagination_plus'>&gt;</button></span>"
paginationContainerAssiduites.querySelector('#paginationAssi')?.addEventListener('change', (e) => {
currentPageAssiduites = e.target.value;
assiduiteCallBack(array);
})
paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => {
if (currentPageAssiduites > 1) {
currentPageAssiduites--;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + ""
assiduiteCallBack(array);
}
})
paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => {
if (currentPageAssiduites < totalPages) {
currentPageAssiduites++;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + ""
assiduiteCallBack(array);
}
})
} else {
paginationContainerJustificatifs.innerHTML = "<span class='liste_pagination'><button class='pagination_moins'>&lt;</button><select id='paginationJusti'></select><button class='pagination_plus'>&gt;</button></span>"
paginationContainerJustificatifs.querySelector('#paginationJusti')?.addEventListener('change', (e) => {
currentPageJustificatifs = e.target.value;
justificatifCallBack(array);
})
paginationContainerJustificatifs.querySelector('.pagination_moins').addEventListener('click', () => {
if (currentPageJustificatifs > 1) {
currentPageJustificatifs--;
paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageJustificatifs
justificatifCallBack(array);
}
})
paginationContainerJustificatifs.querySelector('.pagination_plus').addEventListener('click', () => {
if (currentPageJustificatifs < totalPages) {
currentPageJustificatifs++;
paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageJustificatifs
justificatifCallBack(array);
}
})
}
for (let i = 1; i <= totalPages; i++) {
const paginationButton = document.createElement("option");
paginationButton.textContent = i;
paginationButton.value = i;
if (assi) {
paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton)
if (i == currentPageAssiduites)
paginationContainerAssiduites.querySelector('#paginationAssi').value = i + "";
} else {
paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton)
if (i == currentPageJustificatifs)
paginationContainerJustificatifs.querySelector('#paginationJusti').value = i + "";
}
}
updateActivePaginationButton(assi);
}
function updateActivePaginationButton(assi = true) {
if (assi) {
const paginationButtons =
paginationContainerAssiduites.querySelectorAll("#paginationContainerAssiduites .pagination-button");
paginationButtons.forEach((button) => {
if (parseInt(button.textContent) === currentPageAssiduites) {
button.classList.add("active");
} else {
button.classList.remove("active");
}
});
} else {
const paginationButtons =
paginationContainerJustificatifs.querySelectorAll("#paginationContainerJustificatifs .pagination-button");
paginationButtons.forEach((button) => {
if (parseInt(button.textContent) === currentPageJustificatifs) {
button.classList.add("active");
} else {
button.classList.remove("active");
}
});
}
}
function loadAll() {
try { getAssi(assiduiteCallBack) } catch { }
try { getJusti(justificatifCallBack) } catch { }
}
function order(keyword, callback = () => { }, el, assi = true) {
const call = (array, ordered) => {
const sorted = array.sort((a, b) => {
let keyValueA = a[keyword];
let keyValueB = b[keyword];
if (keyword.indexOf("date") != -1) {
keyValueA = new Date(Date.removeUTC(keyValueA))
keyValueB = new Date(Date.removeUTC(keyValueB))
}
if (keyword.indexOf("module") != -1) {
keyValueA = getModuleImpl(a);
keyValueB = getModuleImpl(b);
}
if (keyword.indexOf("etudid") != -1) {
keyValueA = getEtudiant(a.etudid);
keyValueB = getEtudiant(b.etudid);
keyValueA = `${keyValueA.prenom.capitalize()} ${keyValueA.nom.toUpperCase()}`
keyValueB = `${keyValueB.prenom.capitalize()} ${keyValueB.nom.toUpperCase()}`
}
let orderDertermined = keyValueA > keyValueB;
if (!ordered) {
orderDertermined = keyValueA < keyValueB;
}
return orderDertermined
});
callback(sorted);
};
if (assi) {
orderAssiduites = !orderAssiduites;
getAssi((a) => { call(a, orderAssiduites) });
} else {
orderJustificatifs = !orderJustificatifs;
getJusti((a) => { call(a, orderJustificatifs) });
}
}
function columnTranslator(colName) {
switch (colName) {
case "date_debut":
return "Début";
case "entry_date":
return "Saisie le";
case "date_fin":
return "Fin";
case "etat":
return "État";
case "moduleimpl_id":
return "Module";
case "est_just":
return "Justifiée";
case "raison":
return "Raison";
case "fichier":
return "Fichier";
case "etudid":
return "Etudiant";
case "formsemestre":
return "Semestre";
}
}
function openContext(e) {
e.preventDefault();
selectedRow = e.target.parentElement;
contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`;
contextMenu.style.left = `${e.clientX}px`;
contextMenu.style.display = "block";
if (contextMenu.childElementCount > 3) {
contextMenu.removeChild(contextMenu.lastElementChild)
}
if (selectedRow.getAttribute('type') == "assiduite") {
const li = document.createElement('li')
li.textContent = "Justifier"
let obj_id = selectedRow.getAttribute('obj_id');
let assi = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id })[0]
li.addEventListener('click', () => {
if (assi && !assi.est_just && assi.etat != "PRESENT") {
fastJustify(assi)
} else {
openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée."))
}
})
if (assi && assi.etat != "PRESENT") {
contextMenu.appendChild(li);
}
}
}
function downloadStr(data, name) {
const blob = new Blob([data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', name);
a.click()
a.remove()
}
function askDownload(data) {
const div = document.createElement('div');
const head = document.createElement('h3');
const input = document.createElement('input');
head.textContent = "Veuillez nommer le fichier qui sera téléchargé (sera au format CSV)"
input.type = "text";
input.placeholder = "liste.csv"
div.appendChild(head)
div.appendChild(input)
openPromptModal("Préparation du téléchargement", div, () => {
downloadStr(data, input.value ? input.value : "download.csv")
}, () => { }, "var(--color-present)");
}
function toCSV(array, filters) {
array = filterArray(array, filters.filters)
let csv = filters.columns.map((c) => columnTranslator(c)).join(',') + "\n";
array.forEach((a) => {
let line = ""
filters.columns.forEach((c) => {
switch (c) {
case "fichier":
line += a[c] ? "Oui," : "Non,"
break;
case "etudid":
const e = getEtudiant(a.etudid);
line += `${e.nom.toUpperCase()} ${e.prenom.capitalize()},`
break;
case "formsemestre":
line += a.hasOwnProperty("formsemestre") ? a.formsemestre.title : ""
line += ","
break;
case "est_just":
line += a[c] ? "Oui," : "Non,"
break;
case "moduleimpl_id":
line += `${getModuleImpl(a)},`
break;
default:
line += `${a[c]},`;
break;
}
})
line = line.substring(0, line.lastIndexOf(',')) + "\n"
csv += line;
})
askDownload(csv);
}
function getEtudiant(id) {
if (id in etuds) {
return etuds[id];
}
getSingleEtud(id);
return etuds[id];
}
</script>
<style>
.pageContent {
width: 100%;
max-width: var(--sco-content-max-width);
display: flex;
flex-direction: column;
flex-wrap: wrap;
}
table {
border-collapse: collapse;
text-align: left;
margin: 20px 0;
}
th,
td {
border: 1px solid #dddddd;
padding: 8px;
color: var(--color-default-text);
}
th {
background-color: #f2f2f2;
}
tr:hover {
filter: brightness(1.2)
}
.context-menu {
display: none;
position: fixed;
list-style-type: none;
padding: 10px 0;
background-color: #f9f9f9;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
cursor: pointer;
z-index: 45;
}
.context-menu li {
padding: 8px 16px;
background-color: #f9f9f9;
}
.context-menu li:hover {
filter: brightness(0.7);
}
#deleteOption {
background-color: var(--color-absent);
}
.l-present {
background-color: var(--color-present)
}
.l-absent,
.l-invalid {
background-color: var(--color-absent-clair);
}
.l-valid {
background-color: var(--color-justi-clair);
}
.l-retard {
background-color: var(--color-retard);
}
.l-absent.est_just {
background-color: var(--color-absent-justi);
}
.l-retard.est_just {
background-color: var(--color-retard-justi);
}
/* Ajoutez des styles pour le conteneur de pagination et les boutons */
.pagination-container {
display: flex;
justify-content: center;
margin: 20px 0;
}
.pagination-button {
padding: 10px;
border: 1px solid #ccc;
cursor: pointer;
background-color: #f9f9f9;
margin: 0 5px;
text-decoration: none;
color: #000;
}
.pagination-button:hover {
background-color: #ddd;
}
.pagination-button.active {
background-color: var(--color-primary);
color: #fff;
border-color: var(--color-primary);
}
th>div {
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-head {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.filter-line {
display: flex;
justify-content: start;
align-items: center;
margin: 15px;
}
.filter-line>* {
margin-right: 5px;
}
.rbtn {
width: 35px;
height: 35px;
margin: 0 5px !important;
}
.f-label {
margin: 0 5px;
}
.chk {
margin-left: 2px !important;
}
.filter-body label {
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin: 0;
}
.obj-title {
text-decoration: underline #bbb;
font-weight: bold;
}
.obj-part {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 33%;
padding: 5px;
border: 1px solid #bbb;
}
.obj-dates,
.obj-mod,
.obj-rest {
display: flex;
justify-content: space-evenly;
margin: 2px;
}
.liste_pagination {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 5px;
}
</style>

View File

@ -1,679 +0,0 @@
<table id="justificatifTable">
<thead>
<tr>
<th>
<div>
<span>Début</span>
<a class="icon order" onclick="order('date_debut', justificatifCallBack, this, false)"></a>
</div>
</th>
<th>
<div>
<span>Fin</span>
<a class="icon order" onclick="order('date_fin', justificatifCallBack, this, false)"></a>
</div>
</th>
<th>
<div>
<span>État</span>
<a class="icon order" onclick="order('etat', justificatifCallBack, this, false)"></a>
</div>
</th>
<th>
<div>
<span>Raison</span>
<a class="icon order" onclick="order('raison', justificatifCallBack, this, false)"></a>
</div>
</th>
<th>
<div>
<span>Fichier</span>
<a class="icon order" onclick="order('fichier', justificatifCallBack, this, false)"></a>
</div>
</th>
</tr>
</thead>
<tbody id="tableBodyJustificatifs">
</tbody>
</table>
<div id="paginationContainerJustificatifs" class="pagination-container">
</div>
<script>
const paginationContainerJustificatifs = document.getElementById("paginationContainerJustificatifs");
let currentPageJustificatifs = 1;
let orderJustificatifs = true;
let filterJustificatifs = {
columns: [
"entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"
],
filters: {}
}
const tableBodyJustificatifs = document.getElementById("tableBodyJustificatifs");
function justificatifCallBack(justi) {
justi = filterArray(justi, filterJustificatifs.filters)
renderTableJustificatifs(currentPageJustificatifs, justi);
renderPaginationButtons(justi, false);
}
function renderTableJustificatifs(page, justificatifs) {
generateTableHead(filterJustificatifs.columns, false)
tableBodyJustificatifs.innerHTML = "";
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
justificatifs.slice(start, end).forEach((justificatif) => {
const row = document.createElement("tr");
row.setAttribute('type', "justificatif");
row.setAttribute('obj_id', justificatif.justif_id);
const etat = justificatif.etat.toLowerCase();
if (etat == "valide") {
row.classList.add(`l-valid`);
} else {
row.classList.add(`l-invalid`);
}
filterJustificatifs.columns.forEach((k) => {
const td = document.createElement('td');
if (k.indexOf('date') != -1) {
td.textContent = new Date(Date.removeUTC(justificatif[k])).format(`DD/MM/Y HH:mm`)
} else if (k.indexOf('fichier') != -1) {
td.textContent = justificatif.fichier ? "Oui" : "Non";
} else if (k.indexOf('etudid') != -1) {
const e = getEtudiant(justificatif.etudid);
td.innerHTML = `<a class="etudinfo" id="line-${justificatif.etudid}" href="bilan_etud?etudid=${justificatif.etudid}">${e.prenom.capitalize()} ${e.nom.toUpperCase()}</a>`;
} else if (k == "formsemestre") {
if (justificatif.hasOwnProperty("formsemestre")) {
td.textContent = justificatif.formsemestre.title.replaceAll('-', ' ');
} else {
td.textContent = `Pas de Semestre`;
}
}
else {
if (justificatif[k] != null) {
td.textContent = `${justificatif[k]}`.capitalize()
}
else {
td.textContent = "";
}
}
row.appendChild(td)
})
row.addEventListener("contextmenu", openContext);
tableBodyJustificatifs.appendChild(row);
});
updateActivePaginationButton(false);
}
function detailJustificatifs(justi_id) {
const path = getUrl() + `/api/justificatif/${justi_id}`;
async_get(
path,
(data) => {
const user = getUser(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.raison == null ? "" : data.raison;
const id = data.justif_id;
const fichier = data.fichier != null ? "Oui" : "Non";
let filenames = []
let totalFiles = 0;
if (fichier) {
sync_get(path + "/list", (data2) => {
filenames = data2.filenames;
totalFiles = data2.total;
})
}
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">Raison</span>
<span class="obj-content">${desc}</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">Créé par</span>
<span class="obj-content">${user}</span>
</div>
</div>
<div class="obj-rest">
<div id="est_just" class="obj-part obj-66">
<span class="obj-title">Fichier(s)</span>
<div class="obj-content" id="fich-content"></div>
</div>
<div id="id" class="obj-part">
<span class="obj-title">Identifiant du justificatif</span>
<span class="obj-content">${id}</span>
</div>
</div>
</div>
`
const el = document.createElement('div');
el.innerHTML = html;
const fichContent = el.querySelector('#fich-content');
const s = document.createElement('span')
s.textContent = `${totalFiles} fichier(s) dont ${filenames.length} visible(s)`
fichContent.appendChild(s)
filenames.forEach((name) => {
const a = document.createElement('a');
a.textContent = name
a.classList.add("fich-file")
a.onclick = () => { downloadFile(id, name) };
fichContent.appendChild(a);
})
openAlertModal("Détails", el.firstElementChild, null, "var(--color-information)")
}
)
}
function downloadFile(id, name) {
const path = getUrl() + `/api/justificatif/${id}/export/${name}`;
fetch(path, {
method: "POST"
})
// This returns a promise inside of which we are checking for errors from the server.
// The catch promise at the end of the call does not getting called when the server returns an error.
// More information about the error catching can be found here: https://www.tjvantoll.com/2015/09/13/fetch-and-errors/.
.then((result) => {
if (!result.ok) {
throw Error(result.statusText);
}
// We are reading the *Content-Disposition* header for getting the original filename given from the server
const header = result.headers.get('Content-Disposition');
const parts = header.split(';');
filename = parts[1].split('=')[1].replaceAll("\"", "");
return result.blob();
})
// We use the download property for triggering the download of the file from our browser.
// More information about the following code can be found here: https://stackoverflow.com/questions/32545632/how-can-i-download-a-file-using-window-fetch.
// The filename from the first promise is used as name of the file.
.then((blob) => {
if (blob != null) {
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
}
})
// The catch is getting called only for client-side errors.
// For example the throw in the first then-promise, which is the error that came from the server.
.catch((err) => {
console.error(err);
});
}
function editionJustificatifs(justif_id) {
const path = getUrl() + `/api/justificatif/${justif_id}`;
async_get(
path,
(data) => {
const html = `
<div class="assi-edit">
<div class="justi-row">
<div class="justi-label">
<legend for="justi_date_debut">Date de début</legend>
<input type="datetime-local" name="justi_date_debut" id="justi_date_debut">
</div>
<div class="justi-label">
<legend for="justi_date_fin">Date de fin</legend>
<input type="datetime-local" name="justi_date_fin" id="justi_date_fin">
</div>
</div>
<div class="justi-row">
<div class="justi-label">
<legend for="justi_etat">Etat du justificatif</legend>
<select name="justi_etat" id="justi_etat">
<option value="attente" selected>En Attente de validation</option>
<option value="non_valide">Non Valide</option>
<option value="modifie">Modifié</option>
<option value="valide">Valide</option>
</select>
</div>
</div>
<div class="justi-row">
<div class="justi-label">
<legend for="justi_raison">Raison</legend>
<textarea name="justi_raison" id="justi_raison" cols="50" rows="10" maxlength="500"></textarea>
</div>
</div>
<div class="justi-row">
<div class="justi-sect">
</div>
<div class="justi-label">
<legend for="justi_fich">Importer un fichier</legend>
<input type="file" name="justi_fich" id="justi_fich" multiple>
</div>
</div>
</div>
`
const desc = data.raison == null ? "" : data.raison;
const fichier = data.fichier != null ? "Oui" : "Non";
const el = document.createElement('div')
el.innerHTML = html;
const assiEdit = el.firstElementChild;
assiEdit.querySelector('#justi_etat').value = data.etat.toLowerCase();
assiEdit.querySelector('#justi_raison').value = desc;
const d_deb = new Date(Date.removeUTC(data.date_debut)).format("YYYY-MM-DDTHH:mm")
const d_fin = new Date(Date.removeUTC(data.date_fin)).format("YYYY-MM-DDTHH:mm")
assiEdit.querySelector('#justi_date_debut').value = d_deb
assiEdit.querySelector('#justi_date_fin').value = d_fin
const fichContent = assiEdit.querySelector('.justi-sect');
let filenames = []
let totalFiles = 0;
if (data.fichier) {
sync_get(path + "/list", (data2) => {
filenames = data2.filenames;
totalFiles = data2.total;
})
let html = "<legend>Fichier(s)</legend>"
html += `<span>${totalFiles} fichier(s) dont ${filenames.length} visible(s)</span>`
fichContent.insertAdjacentHTML('beforeend', html)
}
filenames.forEach((name) => {
const a = document.createElement('a');
a.textContent = name
a.classList.add("fich-file")
a.onclick = () => { downloadFile(justif_id, name) };
const input = document.createElement('input')
input.type = "checkbox"
input.name = "destroyFile";
input.classList.add('icon')
const span = document.createElement('span');
span.classList.add('file-line')
span.appendChild(input)
span.appendChild(a)
fichContent.appendChild(span);
})
openPromptModal("Modification du justificatif", assiEdit, () => {
const prompt = document.querySelector('.assi-edit');
let date_debut = prompt.querySelector('#justi_date_debut').value;
let date_fin = prompt.querySelector('#justi_date_fin').value;
if (date_debut == "" || date_fin == "") {
openAlertModal("Dates erronées", document.createTextNode('Les dates sont invalides'));
return true
}
date_debut = new Date(Date.removeUTC(date_debut))
date_fin = new Date(Date.removeUTC(date_fin))
if (date_debut >= date_fin) {
openAlertModal("Dates erronées", document.createTextNode('La date de fin doit être après la date de début'));
return true
}
const edit = {
date_debut: date_debut.format(),
date_fin: date_fin.format(),
raison: prompt.querySelector('#justi_raison').value,
etat: prompt.querySelector('#justi_etat').value,
}
const toRemoveFiles = [...prompt.querySelectorAll('[name="destroyFile"]:checked')]
if (toRemoveFiles.length > 0) {
removeFiles(justif_id, toRemoveFiles);
}
const in_files = prompt.querySelector('#justi_fich');
if (in_files.files.length > 0) {
importNewFiles(justif_id, in_files);
}
fullEditJustificatifs(data.justif_id, edit, () => {
loadAll();
})
}, () => { }, "var(--color-information)");
}
);
}
function fullEditJustificatifs(justif_id, obj, call = () => { }) {
const path = getUrl() + `/api/justificatif/${justif_id}/edit`;
async_post(
path,
obj,
call,
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
function removeFiles(justif_id, files = []) {
const path = getUrl() + `/api/justificatif/${justif_id}/remove`;
files = files.map((el) => {
return el.parentElement.querySelector('a').textContent;
});
sync_post(
path,
{
"remove": "list",
"filenames": files,
},
);
}
function importNewFiles(justif_id, in_files) {
const path = getUrl() + `/api/justificatif/${justif_id}/import`;
const requests = []
Array.from(in_files.files).forEach((f) => {
const fd = new FormData();
fd.append('file', f);
requests.push(
$.ajax(
{
url: path,
type: 'POST',
data: fd,
dateType: 'json',
contentType: false,
processData: false,
success: () => { },
}
)
)
});
$.when(
requests
).done(() => {
})
}
function filterJusti(dept = false) {
let dept_html_head = `
<label>
Semestre
<input class="chk" type="checkbox" name="formsemestre" id="formsemestre" checked>
</label>
<label>
Etudiant
<input class="chk" type="checkbox" name="etudid" id="etudid" checked>
</label>
`
let dept_html_body = `
<span class="filter-line">
<span class="filter-title" for="formsemestre">Recherche dans les semestre</span>
<input type="text" name="formsemestre" id="formsemestre" placeholder="S1 2023" >
</span>
`
let html = `
<div class="filter-body">
<h3>Affichage des colonnes:</h3>
<div class="filter-head">
${dept ? dept_html_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>
Raison
<input class="chk" type="checkbox" name="raison" id="raison" checked>
</label>
<label>
Fichier
<input class="chk" type="checkbox" name="fichier" id="fichier" 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>
<label>
Valide
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="valide">
</label>
<label>
Non Valide
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="non_valide">
</label>
<label>
En Attente
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="attente">
</label>
<label>
Modifié
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="modifie">
</label>
</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>
${dept ? dept_html_body : ""}
</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", "raison", "fichier"];
if (dept) { cols.push("formsemestre") }
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 = filterJustificatifs.columns.includes(k)
label.appendChild(s)
label.appendChild(input)
filterHead.appendChild(label)
})
// Mise à jour des filtres
Object.keys(filterJustificatifs.filters).forEach((key) => {
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
if (key.indexOf('date') != -1) {
l.querySelector(`#${key}_pref`).value = filterJustificatifs.filters[key].pref;
l.querySelector(`#${key}_time`).value = filterJustificatifs.filters[key].time.format("YYYY-MM-DDTHH:mm");
} else if (key.indexOf('etat') != -1) {
l.querySelectorAll('input').forEach((e) => {
e.checked = filterJustificatifs.filters[key].includes(e.value)
})
} else if (key == "formsemestre") {
l.querySelector('#formsemestre').value = filterJustificatifs.filters["formsemestre"];
} else if (key == "etudid") {
l.querySelector('#etudid').value = filterJustificatifs.filters["etud"];
}
})
openPromptModal("Filtrage des Justificatifs", html, () => {
const columns = [...document.querySelectorAll('.chk')]
.map((el) => { if (el.checked) return el.id })
.filter((el) => el)
filterJustificatifs.columns = columns
filterJustificatifs.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 != "") {
filterJustificatifs.filters[key] = {
pref: pref,
time: new Date(Date.removeUTC(time))
}
}
} else if (key.indexOf('etat') != -1) {
filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
} else if (key == "formsemestre") {
filterJustificatifs.filters["formsemestre"] = l.querySelector('#formsemestre').value;
} else if (key == "etud") {
filterJustificatifs.filters["etud"] = l.querySelector('#etud').value;
}
})
loadAll();
}, () => { }, "var(--color-primary)");
}
function downloadJusti() {
getJusti((d) => { toCSV(d, filterJustificatifs) })
}
function getJusti(action) {
try { getAllJustificatifsFromEtud(etudid, action, true, assi_limit_annee) } catch (_) { }
}
</script>
<style>
.fich-file {
cursor: pointer;
margin: 2px;
}
#fich-content {
display: flex;
flex-wrap: wrap;
flex-direction: column;
align-items: center;
}
.obj-66 {
width: 66%;
}
.file-line {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 5px;
}
</style>

View File

@ -56,8 +56,8 @@
</h2>
<b>Absences</b>
{% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'] }}
au {{ sco.etud_cur_sem['date_fin'] }}">({{sco.prefs["assi_metrique"]}})
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
<br />{{'%1.0f'|format(sco.nbabsjust)}} J., {{'%1.0f'|format(sco.nbabsnj)}} N.J.</span>
{% endif %}
<ul>

View File

@ -74,8 +74,9 @@ class ScoData:
if ins:
self.etud_cur_sem = ins.formsemestre
(
self.nbabs,
self.nbabsnj,
self.nbabsjust,
self.nbabs,
) = sco_assiduites.get_assiduites_count_in_interval(
etud.id,
self.etud_cur_sem.date_debut.isoformat(),
@ -84,7 +85,6 @@ class ScoData:
sco_preferences.get_preference("assi_metrique")
),
)
self.nbabsnj = self.nbabs - self.nbabsjust
else:
self.etud_cur_sem = None
else:

View File

@ -64,7 +64,7 @@ from app.models import (
)
from app.scodoc.codes_cursus import UE_STANDARD
from app.auth.models import User
from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified
from app.models.assiduites import get_assiduites_justif
from app.tables.list_etuds import RowEtud, TableEtud
import app.tables.liste_assiduites as liste_assi
@ -468,7 +468,7 @@ def _record_assiduite_etud(
)
# On met à jour les assiduités en fonction du nouveau justificatif
compute_assiduites_justified(etud.id, [justi])
justi.justifier_assiduites()
# Invalider cache
scass.simple_invalidate_cache(ass.to_dict(), etud.id)
@ -778,6 +778,7 @@ def _record_justificatif_etud(
form.date_debut.data = dt_debut_tz_server
form.date_fin.data = dt_fin_tz_server
form.entry_date.data = dt_entry_date_tz_server
justif.dejustifier_assiduites()
if justif.edit_from_form(form):
message = "Justificatif modifié"
@ -792,7 +793,6 @@ def _record_justificatif_etud(
)
else:
message = "Pas de modification"
else:
justif = Justificatif.create_justificatif(
etud,
@ -816,7 +816,7 @@ def _record_justificatif_etud(
# pour utiliser le "reset" (remise en "non_just") des assiduités
# (à terme, il faudrait ne recalculer que les assiduités impactées)
# VOIR TODO dans compute_assiduites_justified
compute_assiduites_justified(etud.id, reset=True)
justif.justifier_assiduites()
scass.simple_invalidate_cache(justif.to_dict(), etud.id)
flash(message)
return True
@ -1595,7 +1595,7 @@ def tableau_assiduite_actions():
user_id=current_user.id,
)
compute_assiduites_justified(objet.etudiant.id, [justificatif_correspondant])
justificatif_correspondant.justifier_assiduites()
scass.simple_invalidate_cache(
justificatif_correspondant.to_dict(), objet.etudiant.id
)
@ -1707,9 +1707,10 @@ def _action_modifier_justificatif(justi: Justificatif):
justi.fichier = archive_name
justi.dejustifier_assiduites()
db.session.add(justi)
db.session.commit()
scass.compute_assiduites_justified(justi.etudid, reset=True)
justi.justifier_assiduites()
scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid)
@ -2181,12 +2182,8 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s
"""
# récupération des ues du semestre
ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = ntc.get_ues_stat_dict()
modimpls_list: list[dict] = []
for ue in ues:
# Ajout des moduleimpl de chaque ue dans la liste des moduleimpls
modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])
modimpls_list: list[dict] = ntc.get_modimpls_dict()
# prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre
selected = "" if moduleimpl_id is not None else "selected"

View File

@ -1187,14 +1187,18 @@ def view_module_abs(moduleimpl_id, fmt="html"):
rows = []
for etud in inscrits:
nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(
(
nb_abs_nj,
nb_abs_just,
nb_abs,
) = sco_assiduites.formsemestre_get_assiduites_count(
etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id
)
rows.append(
{
"nomprenom": etud.nomprenom,
"just": nb_abs_just,
"nojust": nb_abs - nb_abs_just,
"nojust": nb_abs_nj,
"total": nb_abs,
"_nomprenom_target": url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id

View File

@ -38,6 +38,8 @@ JUSTIFICATIFS_FIELDS = {
"external_data": dict,
}
DEPT_JUSTIFICATIFS_FIELDS = JUSTIFICATIFS_FIELDS | {"formsemestre": dict | None}
CREATE_FIELD = {"justif_id": int, "couverture": list}
BATCH_FIELD = {"errors": list, "success": list}
@ -169,6 +171,32 @@ def test_route_justificatifs(api_headers):
check_failure_get(f"/justificatifs/{FAUX}/query?", api_headers)
def test_route_justificatifs_formsemestre(api_headers):
"""test de la route /justificatifs/formsemestre/<int:formsemestre_id>"""
# Bon fonctionnement
data = GET(path="/justificatifs/formsemestre/1", headers=api_headers)
assert isinstance(data, list)
for just in data:
check_fields(just, JUSTIFICATIFS_FIELDS)
# Mauvais fonctionnement
check_failure_get(path="/justificatifs/formsemestre/42069", headers=api_headers)
def test_justificatifs_dept(api_headers):
"""test de la route /justificatifs/dept/<int:dept_id>"""
# Bon fonctionnement
data = GET(path="/justificatifs/dept/1", headers=api_headers)
assert isinstance(data, list)
for just in data:
check_fields(just, DEPT_JUSTIFICATIFS_FIELDS)
# Mauvais fonctionnement
check_failure_get(path="/justificatifs/dept/42069", headers=api_headers)
def test_route_create(api_admin_headers):
"""test de la route /justificatif/<justif_id:int>/create"""
# -== Unique ==-

View File

@ -20,7 +20,6 @@ from app.models import (
ModuleImpl,
Absence,
)
from app.models.assiduites import compute_assiduites_justified
from app.scodoc.sco_exceptions import ScoValueError
from tests.unit import sco_fake_gen
@ -73,8 +72,6 @@ def test_general(test_client):
verifier_comptage_et_filtrage_assiduites(etuds, moduleimpls[:4], formsemestres)
verifier_filtrage_justificatifs(etuds[0], justificatifs)
essais_cache(etuds[0].etudid, formsemestres[:2], moduleimpls)
editer_supprimer_assiduites(etuds, moduleimpls)
editer_supprimer_justificatif(etuds[0])
@ -403,54 +400,6 @@ def _get_justi(
).first()
def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]):
"""Vérification des fonctionnalités du cache"""
# TODO faire un test séparé du test_general
# voir test_calcul_assiduites pour faire
date_deb: str = "2022-09-01T07:00"
date_fin: str = "2023-01-31T19:00"
assiduites_count_no_cache = scass.get_assiduites_count_in_interval(
etudid, date_deb, date_fin
)
assiduites_count_cache = scass.get_assiduites_count_in_interval(
etudid, date_deb, date_fin
)
assert (
assiduites_count_cache == assiduites_count_no_cache == (2, 1)
), "Erreur cache classique"
assert scass.formsemestre_get_assiduites_count(etudid, sems[0]) == (
2,
1,
), "Erreur formsemestre_get_assiduites_count (sans module) A"
assert scass.formsemestre_get_assiduites_count(etudid, sems[1]) == (
0,
0,
), "Erreur formsemestre_get_assiduites_count (sans module) B"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[0].id
) == (
1,
1,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[1].id
) == (
1,
0,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
assert scass.formsemestre_get_assiduites_count(
etudid, sems[0], moduleimpl_id=moduleimpls[2].id
) == (
0,
0,
), "Erreur formsemestre_get_assiduites_count (avec module) A"
def ajouter_justificatifs(etud):
"""test de l'ajout des justificatifs"""
@ -498,10 +447,9 @@ def ajouter_justificatifs(etud):
)
db.session.add(just_obj)
db.session.commit()
just_obj.justifier_assiduites()
justificatifs.append(just_obj)
compute_assiduites_justified(etud.etudid, justificatifs)
# Vérification de la création des justificatifs
assert [
justi for justi in justificatifs if not isinstance(justi, Justificatif)
@ -1416,6 +1364,7 @@ def test_cas_justificatifs(test_client):
Tests de certains cas particuliers des justificatifs
- Création du justificatif avant ou après assiduité
- Assiduité complétement couverte ou non
- Modification de la couverture (edition du justificatif)
"""
data = _setup_fake_db(
@ -1462,7 +1411,7 @@ def test_cas_justificatifs(test_client):
etat=scu.EtatJustificatif.VALIDE,
)
compute_assiduites_justified(etud_1.etudid, [justif_2])
justif_2.justifier_assiduites()
assert len(scass.justifies(justif_2)) == 1, "Justification non prise en compte (b1)"
@ -1496,7 +1445,8 @@ def test_cas_justificatifs(test_client):
)
# Mise à jour de l'assiduité
compute_assiduites_justified(etud_1.etudid, [justif_3, justif_4])
justif_3.justifier_assiduites()
justif_4.justifier_assiduites()
assert (
len(scass.justifies(justif_3)) == 1
@ -1504,3 +1454,279 @@ def test_cas_justificatifs(test_client):
assert (
len(scass.justifies(justif_4)) == 0
), "Justification complète non prise en compte (c2)"
# <- Vérification modification de la couverture ->
# Deux assiduités, 8/01/2024 de 8h à 10h et 14h à 16h
assi_2: Assiduite = Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T08:00", True),
date_fin=scu.is_iso_formated("2024-01-08T10:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
assi_3: Assiduite = Assiduite.create_assiduite(
etud=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T14:00", True),
date_fin=scu.is_iso_formated("2024-01-08T16:00", True),
etat=scu.EtatAssiduite.ABSENT,
)
# <=>Justification complète<=>
# les deux assiduités sont couvertes
justif_5: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T00:00:00", True),
date_fin=scu.is_iso_formated("2024-01-08T23:59:59", True),
etat=scu.EtatJustificatif.VALIDE,
)
# Justification des assiduités
assi_ids: list[int] = justif_5.justifier_assiduites()
assert len(assi_ids) == 2, "Vérification Modification couverture (d1)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d2)"
assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d3)"
assert assi_2.est_just is True, "Vérification Modification couverture (d4)"
assert assi_3.est_just is True, "Vérification Modification couverture (d5)"
# Déjustification des assiduités
justif_5.dejustifier_assiduites()
assi_ids: list[int] = justif_5.dejustifier_assiduites()
assert len(assi_ids) == 2, "Vérification Modification couverture (d6)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d7)"
assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d8)"
assert assi_2.est_just is False, "Vérification Modification couverture (d9)"
assert assi_3.est_just is False, "Vérification Modification couverture (d10)"
# <=>Justification Partielle<=>
# Seule la première assiduité est couverte
justif_5.date_fin = scu.is_iso_formated("2024-01-08T11:00", True)
db.session.add(justif_5)
db.session.commit()
assi_ids: list[int] = justif_5.justifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (e1)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e2)"
assert (
assi_3.assiduite_id not in assi_ids
), "Vérification Modification couverture (e3)"
assert assi_2.est_just is True, "Vérification Modification couverture (e4)"
assert assi_3.est_just is False, "Vérification Modification couverture (e5)"
assi_ids: list[int] = justif_5.dejustifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (e6)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e7)"
assert (
assi_3.assiduite_id not in assi_ids
), "Vérification Modification couverture (e3)"
assert assi_2.est_just is False, "Vérification Modification couverture (e8)"
assert assi_3.est_just is False, "Vérification Modification couverture (e9)"
# <=>Justification Multiple<=>
# Deux justificatifs couvrent une même assiduité
# on justifie la première assiduité avec le premier justificatif
justif_5.justifier_assiduites()
# deuxième justificatif
justif_6: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T08:00", True),
date_fin=scu.is_iso_formated("2024-01-08T10:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
assi_ids: list[int] = justif_6.justifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (f1)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f2)"
assert (
assi_3.assiduite_id not in assi_ids
), "Vérification Modification couverture (f3)"
assert assi_2.est_just is True, "Vérification Modification couverture (f4)"
assert assi_3.est_just is False, "Vérification Modification couverture (f5)"
# on déjustifie le justificatif 5
justif_5.etat = scu.EtatJustificatif.NON_VALIDE
db.session.add(justif_5)
db.session.commit()
assi_ids: list[int] = justif_5.dejustifier_assiduites()
assert len(assi_ids) == 0, "Vérification Modification couverture (f6)"
assert (
assi_2.assiduite_id not in assi_ids
), "Vérification Modification couverture (f7)"
assert assi_2.est_just is True, "Vérification Modification couverture (f8)"
# on déjustifie le justificatif 6
justif_6.etat = scu.EtatJustificatif.NON_VALIDE
db.session.add(justif_6)
db.session.commit()
assi_ids: list[int] = justif_6.dejustifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (f9)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f10)"
assert assi_2.est_just is False, "Vérification Modification couverture (f11)"
# <=>Justification Chevauchée<=>
# 1 justificatif chevauche une assiduité (8h -> 10h) (9h -> 11h)
justif_7: Justificatif = Justificatif.create_justificatif(
etudiant=etud_1,
date_debut=scu.is_iso_formated("2024-01-08T09:00", True),
date_fin=scu.is_iso_formated("2024-01-08T11:00", True),
etat=scu.EtatJustificatif.VALIDE,
)
assi_ids: list[int] = justif_7.justifier_assiduites()
assert len(assi_ids) == 0, "Vérification Modification couverture (g1)"
assert (
assi_2.assiduite_id not in assi_ids
), "Vérification Modification couverture (g2)"
assert assi_2.est_just is False, "Vérification Modification couverture (g3)"
# Modification pour correspondre à l'assiduité
justif_7.date_debut = scu.is_iso_formated("2024-01-08T08:00", True)
db.session.add(justif_7)
db.session.commit()
assi_ids: list[int] = justif_7.justifier_assiduites()
assert len(assi_ids) == 1, "Vérification Modification couverture (g4)"
assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (g5)"
assert assi_2.est_just is True, "Vérification Modification couverture (g6)"
def test_cache_assiduites(test_client):
"""Vérification du bon fonctionnement du cache des assiduités"""
data = _setup_fake_db(
[("2024-01-01", "2024-06-30"), ("2024-07-01", "2024-12-31")],
1,
1,
)
formsemestre1: FormSemestre = data["formsemestres"][0]
formsemestre2: FormSemestre = data["formsemestres"][1]
moduleimpl: ModuleImpl = data["moduleimpls"][0]
etud: Identite = data["etuds"][0]
# Création des assiduités
assiduites: list[dict] = [
# Semestre 1
{
"date_debut": "2024-01-08T08:00",
"date_fin": "2024-01-08T10:00",
"moduleimpl": moduleimpl,
},
{
"date_debut": "2024-01-08T14:00",
"date_fin": "2024-01-08T16:00",
"moduleimpl": moduleimpl,
},
{
"date_debut": "2024-01-09T08:00",
"date_fin": "2024-01-09T10:00",
"moduleimpl": None,
},
{
"date_debut": "2024-01-09T14:00",
"date_fin": "2024-01-09T16:00",
"moduleimpl": None,
},
{
"date_debut": "2024-01-10T08:00",
"date_fin": "2024-01-10T10:00",
"moduleimpl": None,
},
{
"date_debut": "2024-01-10T14:00",
"date_fin": "2024-01-10T16:00",
"moduleimpl": moduleimpl,
},
# Semestre 2
{
"date_debut": "2024-07-09T14:00",
"date_fin": "2024-07-09T16:00",
"moduleimpl": None,
},
{
"date_debut": "2024-07-10T08:00",
"date_fin": "2024-07-10T10:00",
"moduleimpl": None,
},
{
"date_debut": "2024-07-10T14:00",
"date_fin": "2024-07-10T16:00",
"moduleimpl": None,
},
]
justificatifs: list[dict] = [
{
"date_debut": "2024-01-10T00:00",
"date_fin": "2024-01-10T23:59",
},
{
"date_debut": "2024-07-09T00:00",
"date_fin": "2024-07-09T23:59",
},
]
# On ajoute les assiduités et les justificatifs
for assi in assiduites:
Assiduite.create_assiduite(
etud=etud,
date_debut=scu.is_iso_formated(assi["date_debut"], True),
date_fin=scu.is_iso_formated(assi["date_fin"], True),
moduleimpl=assi["moduleimpl"],
etat=scu.EtatAssiduite.ABSENT,
)
for justi in justificatifs:
Justificatif.create_justificatif(
etudiant=etud,
date_debut=scu.is_iso_formated(justi["date_debut"], True),
date_fin=scu.is_iso_formated(justi["date_fin"], True),
etat=scu.EtatJustificatif.VALIDE,
).justifier_assiduites()
# Premier semestre 4nj / 2j / 6t
assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6)
assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6)
# ModuleImpl 2nj / 1j / 3t
assert scass.formsemestre_get_assiduites_count(
etud.id, formsemestre1, moduleimpl.id
) == (2, 1, 3)
# Deuxième semestre 2nj / 1j / 3t
assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
# On supprime la première assiduité (sans invalider le cache)
assi: Assiduite = Assiduite.query.filter_by(etudid=etud.id).first()
db.session.delete(assi)
db.session.commit()
# Premier semestre 4nj / 2j / 6t (Identique car cache)
assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6)
assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6)
# ModuleImpl 1nj / 1j / 2t (Change car non cache)
assert scass.formsemestre_get_assiduites_count(
etud.id, formsemestre1, moduleimpl.id
) == (1, 1, 2)
# Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié)
assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)
# On invalide maintenant le cache
scass.invalidate_assiduites_count(etud.id, formsemestre1.to_dict())
# Premier semestre 3nj / 2j / 5t (Change car cache invalidé)
assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (3, 2, 5)
assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (3, 2, 5)
# ModuleImpl 1nj / 1j / 2t (Ne change pas car pas de changement)
assert scass.formsemestre_get_assiduites_count(
etud.id, formsemestre1, moduleimpl.id
) == (1, 1, 2)
# Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié)
assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3)

View File

@ -33,7 +33,6 @@ from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_cursus_dut
from app.scodoc import sco_saisie_notes
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime
from app.models.assiduites import compute_assiduites_justified
DEPT = TestConfig.DEPT_TEST
@ -192,7 +191,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre:
etudid = etuds[0]["etudid"]
_signal_absences_justificatifs(etudid)
nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem)
_, nbabsjust, nbabs = scass.get_assiduites_count(etudid, sem)
assert nbabs == 6, f"incorrect nbabs ({nbabs})"
assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})"
@ -267,8 +266,5 @@ def _signal_absences_justificatifs(etudid: int):
etat=EtatJustificatif.VALIDE,
)
db.session.add(justif)
compute_assiduites_justified(
etud.etudid,
[justif],
)
db.session.commit()
justif.justifier_assiduites()

View File

@ -396,7 +396,7 @@ def ajouter_assiduites_justificatifs(formsemestre: FormSemestre):
for etud in formsemestre.etuds:
base_date = datetime.datetime(
2022, 9, [5, 12, 19, 26][random.randint(0, 3)], 8, 0, 0
2021, 9, [6, 13, 20, 27][random.randint(0, 3)], 8, 0, 0
)
base_date = localize_datetime(base_date)