diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index fc680dbc717c3f72a698c1c3660e49291b098a35..4d00821459b302381d4e39c85023e6eb9c1b3429 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -386,6 +386,12 @@ class BulletinBUT: semestre_infos["absences"] = { "injustifie": nbabs - nbabsjust, "total": nbabs, + "metrique": { + "H.": "Heure(s)", + "J.": "Journée(s)", + "1/2 J.": "1/2 Jour.", + "N.": "Nombre", + }.get(sco_preferences.get_preference("assi_metrique")), } decisions_ues = self.res.get_etud_decisions_ue(etud.id) or {} if self.prefs["bul_show_ects"]: diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 68399262b628892a31c4b015f826cb794b19c888..eef4cdd88597621d44b8ed804373b670065cc8a0 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -39,9 +39,11 @@ from app.models.validations import ScolarFormSemestreValidation from app.scodoc import codes_cursus, sco_preferences from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_utils import MONTH_NAMES_ABBREV +from app.scodoc.sco_utils import MONTH_NAMES_ABBREV, translate_assiduites_metric from app.scodoc.sco_vdi import ApoEtapeVDI +from app.scodoc.sco_utils import translate_assiduites_metric + GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes @@ -678,8 +680,12 @@ class FormSemestre(db.Model): """ from app.scodoc import sco_abs - return sco_abs.get_abs_count_in_interval( - etudid, self.date_debut.isoformat(), self.date_fin.isoformat() + metrique = sco_preferences.get_preference("assi_metrique", self.id) + return sco_abs.get_assiduites_count_in_interval( + etudid, + self.date_debut.isoformat(), + self.date_fin.isoformat(), + translate_assiduites_metric(metrique), ) def get_codes_apogee(self, category=None) -> set[str]: diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index b0d4a234839f84188da59c3bd75eb4a51d96153d..6d09becc294ce66f7357770c3955f05652aa0510 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -116,10 +116,10 @@ def sidebar(etudid: int = None): ) if etud["cursem"]: cur_sem = etud["cursem"] - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, cur_sem) + nbabs, nbabsjust = sco_abs.get_assiduites_count(etudid, cur_sem) nbabsnj = nbabs - nbabsjust H.append( - f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">(1/2 j.) + f"""<span title="absences du { cur_sem["date_debut"] } au { cur_sem["date_fin"] }">({sco_preferences.get_preference("assi_metrique", None)}) <br>{ nbabsjust } J., { nbabsnj } N.J.</span>""" ) H.append("<ul>") diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index e1672492b1893812d772d8cd5dcfbec13bbeb26c..f34ab6d33203b063d6da890ccbff550a443df975 100755 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -1054,19 +1054,42 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): return r -def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso): +def get_assiduites_count(etudid, sem): + """Les comptes d'absences de cet étudiant dans ce semestre: + tuple (nb abs non justifiées, nb abs justifiées) + Utilise un cache. + """ + metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"]) + return get_assiduites_count_in_interval( + etudid, + sem["date_debut_iso"], + sem["date_fin_iso"], + scu.translate_assiduites_metric(metrique), + ) + + +def get_assiduites_count_in_interval( + etudid, date_debut_iso, date_fin_iso, metrique="demi" +): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: tuple (nb abs, nb abs justifiées) Utilise un cache. """ - key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso + "_assiduites" + key = ( + str(etudid) + + "_" + + date_debut_iso + + "_" + + date_fin_iso + + f"{metrique}_assiduites" + ) r = sco_cache.AbsSemEtudCache.get(key) if not r: - date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True) date_fin: datetime.datetime = scu.is_iso_formated(date_fin_iso, True) assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) + assiduites = assiduites.filter(Assiduite.etat != 0) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) assiduites = scass.filter_by_date(assiduites, Assiduite, date_debut, date_fin) @@ -1076,7 +1099,7 @@ def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso): calculator: scass.CountCalculator = scass.CountCalculator() calculator.compute_assiduites(assiduites) - nb_abs: dict = calculator.to_dict()["demi"] + nb_abs: dict = calculator.to_dict()[metrique] abs_just: list[Assiduite] = scass.get_all_justified( etudid, date_debut, date_fin @@ -1084,7 +1107,7 @@ def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso): calculator.reset() calculator.compute_assiduites(abs_just) - nb_abs_just: dict = calculator.to_dict()["demi"] + nb_abs_just: dict = calculator.to_dict()[metrique] r = (nb_abs, nb_abs_just) ans = sco_cache.AbsSemEtudCache.set(key, r) @@ -1101,6 +1124,15 @@ def invalidate_abs_count(etudid, sem): sco_cache.AbsSemEtudCache.delete(key) +def invalidate_assiduites_count(etudid, sem): + """Invalidate (clear) cached counts""" + date_debut = sem["date_debut_iso"] + date_fin = sem["date_fin_iso"] + for met in ["demi", "journee", "compte", "heure"]: + key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites" + sco_cache.AbsSemEtudCache.delete(key) + + def invalidate_abs_count_sem(sem): """Invalidate (clear) cached abs counts for all the students of this semestre""" inscriptions = ( @@ -1112,6 +1144,17 @@ def invalidate_abs_count_sem(sem): invalidate_abs_count(ins["etudid"], sem) +def invalidate_assiduites_count_sem(sem): + """Invalidate (clear) cached abs counts for all the students of this semestre""" + inscriptions = ( + sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( + sem["formsemestre_id"] + ) + ) + for ins in inscriptions: + invalidate_assiduites_count(ins["etudid"], sem) + + def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. Invalide cache absence et caches semestre @@ -1145,3 +1188,38 @@ def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate # Inval cache compteurs absences: invalidate_abs_count_sem(sem) + + +def invalidate_assiduites_etud_date(etudid, date): + """Doit etre appelé à chaque modification des assiduites pour cet étudiant et cette date. + Invalide cache absence et caches semestre + date: date au format ISO + """ + from app.scodoc import sco_compute_moy + + # Semestres a cette date: + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + sems = [ + sem + for sem in etud["sems"] + if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date + ] + + # Invalide les PDF et les absences: + for sem in sems: + # Inval cache bulletin et/ou note_table + if sco_compute_moy.formsemestre_expressions_use_abscounts( + sem["formsemestre_id"] + ): + # certaines formules utilisent les absences + pdfonly = False + else: + # efface toujours le PDF car il affiche en général les absences + pdfonly = True + + sco_cache.invalidate_formsemestre( + formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly + ) + + # Inval cache compteurs absences: + invalidate_assiduites_count(etudid, sem) diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index c01aa4915d0f0c25a841d17c0bd4b60bd0f5e832..88d1d785e8928f05e42c119a8b33846b7d5eb3d7 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -47,6 +47,7 @@ import app.scodoc.notesdb as ndb from app.scodoc import sco_etud from app.scodoc import sco_preferences from app.scodoc import sco_users +from app.scodoc import sco_utils as scu def abs_notify(etudid, date): @@ -61,8 +62,15 @@ def abs_notify(etudid, date): if not formsemestre: return # non inscrit a la date, pas de notification - nbabs, nbabsjust = sco_abs.get_abs_count_in_interval( - etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat() + nbabs, nbabsjust = sco_abs.get_assiduites_count_in_interval( + etudid, + formsemestre.date_debut.isoformat(), + formsemestre.date_fin.isoformat(), + scu.translate_assiduites_metric( + sco_preferences.get_preference( + "assi_metrique", formsemestre.formsemestre_id + ) + ), ) do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust) @@ -85,6 +93,7 @@ def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust): 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: @@ -174,6 +183,8 @@ def abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id): (nbabs > abs_notify_abs_threshold) (nbabs - nbabs_last_notified) > abs_notify_abs_increment + + TODO Mettre à jour avec le module assiduité + fonctionnement métrique """ abs_notify_abs_threshold = sco_preferences.get_preference( "abs_notify_abs_threshold", formsemestre_id diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 2f9624698d22a6b53ac976b33382158fd0eaf3a4..c827207b5518e780b8d5824ebd57a5c0f42aac5e 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -197,7 +197,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_abs.get_abs_count(etudid, nt.sem) + I["nbabs"], I["nbabsjust"] = sco_abs.get_assiduites_count(etudid, nt.sem) # --- Decision Jury infos, dpv = etud_descr_situation_semestre( @@ -487,7 +487,7 @@ def _ue_mod_bulletin( ) # peut etre 'NI' is_malus = mod["module"]["module_type"] == ModuleType.MALUS if bul_show_abs_modules: - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) + nbabs, nbabsjust = sco_abs.get_assiduites_count(etudid, sem) mod_abs = [nbabs, nbabsjust] mod["mod_abs_txt"] = scu.fmt_abs(mod_abs) else: diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index a93f4392e7b08313b2db757a35a11c28fe254219..ec03dec728f78eb859ea62e5827a38526190c14e 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -297,7 +297,7 @@ def formsemestre_bulletinetud_published_dict( # --- Absences if prefs["bul_show_abs"]: - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) + nbabs, nbabsjust = sco_abs.get_assiduites_count(etudid, sem) d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust) # --- Décision Jury diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index e2ad76617bf766c4fcc3a78d5ad82dd2e6073e6e..71845c66627b14b79a15023a50acaeb66a95ccd2 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -63,6 +63,7 @@ from app.scodoc import sco_etud from app.scodoc import sco_xml from app.scodoc.sco_xml import quote_xml_attr + # -------- Bulletin en XML # (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict() # pour simplifier le code, mais attention a la maintenance !) @@ -369,7 +370,7 @@ def make_xml_formsemestre_bulletinetud( # --- Absences if sco_preferences.get_preference("bul_show_abs", formsemestre_id): - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) + nbabs, nbabsjust = sco_abs.get_assiduites_count(etudid, sem) doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust))) # --- Decision Jury if ( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index b2686445ab43a50bd6529b1abaaec1a9f8fcbc37..19af25fe9877bab16e0eb9bdfb7608154a14dc5f 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -696,7 +696,7 @@ 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_abs.get_abs_count(etudid, sem) + nbabs, nbabsjust = sco_abs.get_assiduites_count(etudid, sem) H.append(f"""<td class="rcp_abs">{nbabs - nbabsjust}</td>""") # UEs diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index 605cbc07241b088b4e9cc21048eb5a74cb0ea6f1..46f2599e3eeb37d77a28ba1023f1c7ce622976c5 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -107,7 +107,7 @@ def etud_get_poursuite_info(sem, etud): rangs.append(["rang_" + codeModule, rangModule]) # Absences - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, nt.sem) + nbabs, nbabsjust = sco_abs.get_assiduites_count(etudid, nt.sem) if ( dec and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index ec21d0095463e6084868a8dcc29d04e4f7b45abe..9656f060c2675076b3694509f43f680886d01c44 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -653,6 +653,19 @@ class BasePreferences(object): "explanation": "Liste des jours (lun,mar,mer,jeu,ven,sam,dim)", }, ), + ( + "assi_metrique", + { + "initvalue": "1/2 J.", + "input_type": "menu", + "labels": ["1/2 J.", "J.", "H.", "N."], + "allowed_values": ["1/2 J.", "J.", "H.", "N."], + "title": "Métrique de l'assiduité", + "explanation": "Unité affichée dans la fiche étudiante et le bilan\n(J. = journée, H. = heure, N. = nombre)", + "category": "assi", + "only_global" : True, + }, + ), # portal ( "portal_url", diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index fc369efc0e0920be942fcadd10ed4a4f3f9444ee..8281225c6177134ab841baab08ac623ab4745349 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -139,7 +139,7 @@ def feuille_preparation_jury(formsemestre_id): main_partition_id, "" ) # absences: - e_nbabs, e_nbabsjust = sco_abs.get_abs_count(etud.id, sem) + e_nbabs, e_nbabsjust = sco_abs.get_assiduites_count(etud.id, sem) nbabs[etud.id] = e_nbabs nbabsjust[etud.id] = e_nbabs - e_nbabsjust diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index dbcdfffae402276d7b9abbf396a68a80839244ab..73e0569951455bacac047b41c575884aec2d11d8 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -251,6 +251,17 @@ def is_period_overlapping( return p_deb < i_fin and p_fin > i_deb +def translate_assiduites_metric(hr_metric) -> str: + if hr_metric == "1/2 J.": + return "demi" + if hr_metric == "J.": + return "journee" + if hr_metric == "N.": + return "compte" + if hr_metric == "H.": + return "heure" + + # Types de modules class ModuleType(IntEnum): """Code des types de module.""" diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index c1fac2113b16d73f482e328db337591283a28088..7c6ec4aec9cc800bfb55b0758e5a0462f802f42f 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -1,70 +1,79 @@ /* Module par Seb. L. */ class releveBUT extends HTMLElement { - constructor() { - super(); - this.shadow = this.attachShadow({ mode: 'open' }); - - /* Config par defaut */ - this.config = { - showURL: true - }; - - /* Template du module */ - this.shadow.innerHTML = this.template(); - - /* Style du module */ - const styles = document.createElement('link'); - styles.setAttribute('rel', 'stylesheet'); - if (location.href.includes("ScoDoc")) { - styles.setAttribute('href', removeLastTwoComponents(getCurrentScriptPath()) + '/css/releve-but.css'); // Scodoc - } else { - styles.setAttribute('href', '/assets/styles/releve-but.css'); // Passerelle - } - this.shadow.appendChild(styles); - } - listeOnOff() { - this.parentElement.parentElement.classList.toggle("listeOff"); - this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e => { - e.classList.remove("moduleOnOff") - }) - } - moduleOnOff() { - this.parentElement.classList.toggle("moduleOnOff"); - } - goTo() { - let module = this.dataset.module; - this.parentElement.parentElement.parentElement.parentElement.querySelector("#Module_" + module).scrollIntoView(); - } - - set setConfig(config) { - this.config.showURL = config.showURL ?? this.config.showURL; - } - - set showData(data) { - // this.showInformations(data); - this.showSemestre(data); - this.showSynthese(data); - this.showEvaluations(data); - - this.showCustom(data); - - this.setOptions(data.options); - - this.shadow.querySelectorAll(".CTA_Liste").forEach(e => { - e.addEventListener("click", this.listeOnOff) - }) - this.shadow.querySelectorAll(".ue, .module").forEach(e => { - e.addEventListener("click", this.moduleOnOff) - }) - this.shadow.querySelectorAll(":not(.ueBonus)+.syntheseModule").forEach(e => { - e.addEventListener("click", this.goTo) - }) - - this.shadow.children[0].classList.add("ready"); - } - - template() { - return ` + constructor() { + super(); + this.shadow = this.attachShadow({ mode: "open" }); + + /* Config par defaut */ + this.config = { + showURL: true, + }; + + /* Template du module */ + this.shadow.innerHTML = this.template(); + + /* Style du module */ + const styles = document.createElement("link"); + styles.setAttribute("rel", "stylesheet"); + if (location.href.includes("ScoDoc")) { + styles.setAttribute( + "href", + removeLastTwoComponents(getCurrentScriptPath()) + "/css/releve-but.css" + ); // Scodoc + } else { + styles.setAttribute("href", "/assets/styles/releve-but.css"); // Passerelle + } + this.shadow.appendChild(styles); + } + listeOnOff() { + this.parentElement.parentElement.classList.toggle("listeOff"); + this.parentElement.parentElement + .querySelectorAll(".moduleOnOff") + .forEach((e) => { + e.classList.remove("moduleOnOff"); + }); + } + moduleOnOff() { + this.parentElement.classList.toggle("moduleOnOff"); + } + goTo() { + let module = this.dataset.module; + this.parentElement.parentElement.parentElement.parentElement + .querySelector("#Module_" + module) + .scrollIntoView(); + } + + set setConfig(config) { + this.config.showURL = config.showURL ?? this.config.showURL; + } + + set showData(data) { + // this.showInformations(data); + this.showSemestre(data); + this.showSynthese(data); + this.showEvaluations(data); + + this.showCustom(data); + + this.setOptions(data.options); + + this.shadow.querySelectorAll(".CTA_Liste").forEach((e) => { + e.addEventListener("click", this.listeOnOff); + }); + this.shadow.querySelectorAll(".ue, .module").forEach((e) => { + e.addEventListener("click", this.moduleOnOff); + }); + this.shadow + .querySelectorAll(":not(.ueBonus)+.syntheseModule") + .forEach((e) => { + e.addEventListener("click", this.goTo); + }); + + this.shadow.children[0].classList.add("ready"); + } + + template() { + return ` <div> <div class="wait"></div> <main class="releve"> @@ -140,33 +149,36 @@ class releveBUT extends HTMLElement { </main> </div>`; - } + } - /********************************/ - /* Informations sur l'étudiant */ - /********************************/ - showInformations(data) { - this.shadow.querySelector(".studentPic").src = data.etudiant.photo_url || "default_Student.svg"; + /********************************/ + /* Informations sur l'étudiant */ + /********************************/ + showInformations(data) { + this.shadow.querySelector(".studentPic").src = + data.etudiant.photo_url || "default_Student.svg"; - let output = ''; + let output = ""; - if (this.config.showURL) { - output += `<a href="${data.etudiant.fiche_url}" class=info_etudiant>`; - } else { - output += `<div class=info_etudiant>`; - } + if (this.config.showURL) { + output += `<a href="${data.etudiant.fiche_url}" class=info_etudiant>`; + } else { + output += `<div class=info_etudiant>`; + } - output += ` + output += ` <div class=civilite> ${this.civilite(data.etudiant.civilite)} ${data.etudiant.nom} ${data.etudiant.prenom}`; - if (data.etudiant.date_naissance) { - output += ` <div class=dateNaissance>né${(data.etudiant.civilite == "F") ? "e" : ""} le ${this.ISOToDate(data.etudiant.date_naissance)}</div>`; - } + if (data.etudiant.date_naissance) { + output += ` <div class=dateNaissance>né${ + data.etudiant.civilite == "F" ? "e" : "" + } le ${this.ISOToDate(data.etudiant.date_naissance)}</div>`; + } - output += ` + output += ` </div> <div class=numerosEtudiant> Numéro étudiant : ${data.etudiant.code_nip || "~"} - @@ -174,46 +186,51 @@ class releveBUT extends HTMLElement { </div> <div>${data.formation.titre}</div> `; - if (this.config.showURL) { - output += `</a>`; - } else { - output += `</div>`; - } - - this.shadow.querySelector(".infoEtudiant").innerHTML = output; - } - - /*******************************/ - /* Affichage local */ - /*******************************/ - showCustom(data) { - this.shadow.querySelector(".custom").innerHTML = data.custom || ""; - } - - /*******************************/ - /* Information sur le semestre */ - /*******************************/ - showSemestre(data) { - let correspondanceCodes = { - "ADM": "Admis", - "AJD": "Admis par décision de jury", - "PASD": "Passage de droit : tout n'est pas validé, mais d'après les règles du BUT, vous passez", - "PAS1NCI": "Vous passez par décision de jury mais attention, vous n'avez pas partout le niveau suffisant", - "RED": "Ajourné mais autorisé à redoubler", - "NAR": "Non admis et non autorisé à redoubler : réorientation", - "DEM": "Démission", - "ABAN": "Abandon constaté sans lettre de démission", - "RAT": "En attente d'un rattrapage", - "EXCLU": "Exclusion dans le cadre d'une décision disciplinaire", - "DEF": "Défaillance : non évalué par manque d'assiduité", - "ABL": "Année blanche" - } - - this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `; - this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription); - let output = ''; - if (!data.options.block_moyenne_generale) { - output += ` + if (this.config.showURL) { + output += `</a>`; + } else { + output += `</div>`; + } + + this.shadow.querySelector(".infoEtudiant").innerHTML = output; + } + + /*******************************/ + /* Affichage local */ + /*******************************/ + showCustom(data) { + this.shadow.querySelector(".custom").innerHTML = data.custom || ""; + } + + /*******************************/ + /* Information sur le semestre */ + /*******************************/ + showSemestre(data) { + let correspondanceCodes = { + ADM: "Admis", + AJD: "Admis par décision de jury", + PASD: "Passage de droit : tout n'est pas validé, mais d'après les règles du BUT, vous passez", + PAS1NCI: + "Vous passez par décision de jury mais attention, vous n'avez pas partout le niveau suffisant", + RED: "Ajourné mais autorisé à redoubler", + NAR: "Non admis et non autorisé à redoubler : réorientation", + DEM: "Démission", + ABAN: "Abandon constaté sans lettre de démission", + RAT: "En attente d'un rattrapage", + EXCLU: "Exclusion dans le cadre d'une décision disciplinaire", + DEF: "Défaillance : non évalué par manque d'assiduité", + ABL: "Année blanche", + }; + + this.shadow.querySelector( + "#identite_etudiant" + ).innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `; + this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate( + data.semestre.inscription + ); + let output = ""; + if (!data.options.block_moyenne_generale) { + output += ` <div> <div class=enteteSemestre>Moyenne</div><div class=enteteSemestre>${data.semestre.notes.value}</div> <div class=rang>Rang :</div><div class=rang>${data.semestre.rang.value} / ${data.semestre.rang.total}</div> @@ -222,64 +239,72 @@ class releveBUT extends HTMLElement { <div>Min. promo. :</div><div>${data.semestre.notes.min}</div> </div> `; - } - output += ` + } + output += ` ${(() => { - if ((!data.semestre.rang.groupes) || - (Object.keys(data.semestre.rang.groupes).length == 0)) { - return ""; - } - let output = ""; - let [idGroupe, dataGroupe] = Object.entries(data.semestre.rang.groupes)[0]; - output += `<div> + if ( + !data.semestre.rang.groupes || + Object.keys(data.semestre.rang.groupes).length == 0 + ) { + return ""; + } + let output = ""; + let [idGroupe, dataGroupe] = Object.entries( + data.semestre.rang.groupes + )[0]; + output += `<div> <div class=enteteSemestre>${data.semestre.groupes[0]?.group_name}</div><div></div> <div class=rang>Rang :</div><div class=rang>${dataGroupe.value} / ${dataGroupe.total}</div> </div>`; - // <div>Max. promo. :</div><div>${dataGroupe.max || "-"}</div> - // <div>Moy. promo. :</div><div>${dataGroupe.moy || "-"}</div> - // <div>Min. promo. :</div><div>${dataGroupe.min || "-"}</div> - return output; - })()} + // <div>Max. promo. :</div><div>${dataGroupe.max || "-"}</div> + // <div>Moy. promo. :</div><div>${dataGroupe.moy || "-"}</div> + // <div>Min. promo. :</div><div>${dataGroupe.min || "-"}</div> + return output; + })()} <div class=absencesRecap> - <div class=enteteSemestre>Absences</div><div class=enteteSemestre>1/2 jour.</div> + <div class=enteteSemestre>Absences</div><div class=enteteSemestre>${ + data.semestre.absences?.metrique ?? "1/2 jour." + }</div> <div class=abs>Non justifiées</div> <div>${data.semestre.absences?.injustifie ?? "-"}</div> <div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div> </div>`; - if (data.semestre.decision_rcue?.length) { - output += ` + if (data.semestre.decision_rcue?.length) { + output += ` <div> <div class=enteteSemestre>RCUE</div><div></div> ${(() => { - let output = ""; - data.semestre.decision_rcue.forEach(competence => { - output += `<div class=competence>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`; - }) - return output; - })()} + let output = ""; + data.semestre.decision_rcue.forEach((competence) => { + output += `<div class=competence>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`; + }); + return output; + })()} </div> - </div>` - } - if (data.semestre.decision_ue?.length) { - output += ` + </div>`; + } + if (data.semestre.decision_ue?.length) { + output += ` <div> <div class=enteteSemestre>UE</div><div></div> ${(() => { - let output = ""; - data.semestre.decision_ue.forEach(ue => { - output += `<div class=competence>${ue.acronyme}</div><div>${ue.code}</div>`; - }) - return output; - })()} + let output = ""; + data.semestre.decision_ue.forEach((ue) => { + output += `<div class=competence>${ue.acronyme}</div><div>${ue.code}</div>`; + }); + return output; + })()} </div> - </div>` - } + </div>`; + } - output += ` + output += ` <a class=photo href="${data.etudiant.fiche_url}"> - <img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"> + <img src="${ + data.etudiant.photo_url || "default_Student.svg" + }" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"> </a>`; - /*${data.semestre.groupes.map(groupe => { + /*${data.semestre.groupes.map(groupe => { return ` <div> <div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div> @@ -291,37 +316,42 @@ class releveBUT extends HTMLElement { `; }).join("") }*/ - this.shadow.querySelector(".infoSemestre").innerHTML = output; - + this.shadow.querySelector(".infoSemestre").innerHTML = output; - /*if(data.semestre.decision_annee?.code){ + /*if(data.semestre.decision_annee?.code){ this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code]; }*/ - this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || ""; - /*if (data.semestre.decision?.code) { + this.shadow.querySelector(".decision").innerHTML = + data.semestre.situation || ""; + /*if (data.semestre.decision?.code) { this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || ""); }*/ - this.shadow.querySelector("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis ?? "-") + " / " + (data.semestre.ECTS?.total ?? "-"); - } - - /*******************************/ - /* Synthèse */ - /*******************************/ - showSynthese(data) { - let output = ``; - /* Fusion et tri des UE et UE capitalisées */ - let fusionUE = [ - ...Object.entries(data.ues), - ...Object.entries(data.ues_capitalisees) - ].sort((a, b) => { - return a[1].numero - b[1].numero - }); - - /* Affichage */ - fusionUE.forEach(([ue, dataUE]) => { - if (dataUE.type == 1) { // UE Sport / Bonus - output += ` + this.shadow.querySelector("#ects_tot").innerHTML = + "ECTS : " + + (data.semestre.ECTS?.acquis ?? "-") + + " / " + + (data.semestre.ECTS?.total ?? "-"); + } + + /*******************************/ + /* Synthèse */ + /*******************************/ + showSynthese(data) { + let output = ``; + /* Fusion et tri des UE et UE capitalisées */ + let fusionUE = [ + ...Object.entries(data.ues), + ...Object.entries(data.ues_capitalisees), + ].sort((a, b) => { + return a[1].numero - b[1].numero; + }); + + /* Affichage */ + fusionUE.forEach(([ue, dataUE]) => { + if (dataUE.type == 1) { + // UE Sport / Bonus + output += ` <div> <div class="ue ueBonus"> <h3>Bonus</h3> @@ -330,52 +360,60 @@ class releveBUT extends HTMLElement { ${this.ueSport(dataUE.modules)} </div> `; - } else { - output += ` + } else { + output += ` <div> <div class="ue ${dataUE.date_capitalisation ? "capitalisee" : ""}"> <h3> - ${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""} + ${ue}${dataUE.titre ? " - " + dataUE.titre : ""} </h3> <div> - <div class=moyenne>Moyenne : ${dataUE.moyenne?.value || dataUE.moyenne || "-"}</div> - <div class=ue_rang>Rang : ${dataUE.moyenne?.rang} / ${dataUE.moyenne?.total}</div> + <div class=moyenne>Moyenne : ${ + dataUE.moyenne?.value || dataUE.moyenne || "-" + }</div> + <div class=ue_rang>Rang : ${dataUE.moyenne?.rang} / ${ + dataUE.moyenne?.total + }</div> <div class=info>`; - if (!dataUE.date_capitalisation) { - output += ` Bonus : ${dataUE.bonus || 0} - + if (!dataUE.date_capitalisation) { + output += ` Bonus : ${dataUE.bonus || 0} - Malus : ${dataUE.malus || 0}`; - } else { - output += ` le ${this.ISOToDate(dataUE.date_capitalisation.split("T")[0])} <a href="${dataUE.bul_orig_url}">dans ce semestre</a>`; - } - - output += ` <span class=ects> - - ECTS : ${dataUE.ECTS?.acquis ?? "-"} / ${dataUE.ECTS?.total ?? "-"} + } else { + output += ` le ${this.ISOToDate( + dataUE.date_capitalisation.split("T")[0] + )} <a href="${dataUE.bul_orig_url}">dans ce semestre</a>`; + } + + output += ` <span class=ects> - + ECTS : ${dataUE.ECTS?.acquis ?? "-"} / ${ + dataUE.ECTS?.total ?? "-" + } </span> </div> </div>`; - /*<div class=absences> + /*<div class=absences> <div>Abs N.J.</div><div>${dataUE.absences?.injustifie || 0}</div> <div>Total</div><div>${dataUE.absences?.total || 0}</div> </div>*/ - output += "</div>"; - - if (!dataUE.date_capitalisation) { - output += - this.synthese(data, dataUE.ressources) + - this.synthese(data, dataUE.saes); - } - - output += "</div>"; - } - }); - this.shadow.querySelector(".synthese").innerHTML = output; - } - synthese(data, modules) { - let output = ""; - Object.entries(modules).forEach(([module, dataModule]) => { - let titre = data.ressources[module]?.titre || data.saes[module]?.titre; - //let url = data.ressources[module]?.url || data.saes[module]?.url; - output += ` + output += "</div>"; + + if (!dataUE.date_capitalisation) { + output += + this.synthese(data, dataUE.ressources) + + this.synthese(data, dataUE.saes); + } + + output += "</div>"; + } + }); + this.shadow.querySelector(".synthese").innerHTML = output; + } + synthese(data, modules) { + let output = ""; + Object.entries(modules).forEach(([module, dataModule]) => { + let titre = data.ressources[module]?.titre || data.saes[module]?.titre; + //let url = data.ressources[module]?.url || data.saes[module]?.url; + output += ` <div class=syntheseModule data-module="${module.replace(/[^a-zA-Z0-9]/g, "")}"> <div>${module} - ${titre}</div> <div> @@ -384,14 +422,14 @@ class releveBUT extends HTMLElement { </div> </div> `; - }) - return output; - } - ueSport(modules) { - let output = ""; - Object.values(modules).forEach((module) => { - Object.values(module.evaluations).forEach((evaluation) => { - output += ` + }); + return output; + } + ueSport(modules) { + let output = ""; + Object.values(modules).forEach((module) => { + Object.values(module.evaluations).forEach((evaluation) => { + output += ` <div class=syntheseModule> <div>${module.titre} - ${evaluation.description || "Note"}</div> <div> @@ -400,27 +438,31 @@ class releveBUT extends HTMLElement { </div> </div> `; - }) - }) - return output; - } - - /*******************************/ - /* Evaluations */ - /*******************************/ - showEvaluations(data) { - this.shadow.querySelector(".evaluations").innerHTML = this.module(data.ressources); - this.shadow.querySelector(".sae").innerHTML += this.module(data.saes); - } - module(module) { - let output = ""; - Object.entries(module).forEach(([numero, content]) => { - output += ` + }); + }); + return output; + } + + /*******************************/ + /* Evaluations */ + /*******************************/ + showEvaluations(data) { + this.shadow.querySelector(".evaluations").innerHTML = this.module( + data.ressources + ); + this.shadow.querySelector(".sae").innerHTML += this.module(data.saes); + } + module(module) { + let output = ""; + Object.entries(module).forEach(([numero, content]) => { + output += ` <div id="Module_${numero.replace(/[^a-zA-Z0-9]/g, "")}"> <div class=module> <h3>${this.URL(content.url, `${numero} - ${content.titre}`)}</h3> <div> - <div class=moyenne>Moyenne indicative : ${content.moyenne.value}</div> + <div class=moyenne>Moyenne indicative : ${ + content.moyenne.value + }</div> <div class=info> Classe : ${content.moyenne.moy} - Max : ${content.moyenne.max} - @@ -435,14 +477,14 @@ class releveBUT extends HTMLElement { ${this.evaluation(content.evaluations)} </div> `; - }) - return output; - } - - evaluation(evaluations) { - let output = ""; - evaluations.forEach((evaluation) => { - output += ` + }); + return output; + } + + evaluation(evaluations) { + let output = ""; + evaluations.forEach((evaluation) => { + output += ` <div class=eval> <div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div> <div> @@ -454,52 +496,55 @@ class releveBUT extends HTMLElement { <div>Max. promo.</div><div>${evaluation.note.max}</div> <div>Moy. promo.</div><div>${evaluation.note.moy}</div> <div>Min. promo.</div><div>${evaluation.note.min}</div> - ${Object.entries(evaluation.poids).map(([UE, poids]) => { - return ` + ${Object.entries(evaluation.poids) + .map(([UE, poids]) => { + return ` <div>Poids ${UE}</div> <div>${poids}</div> `; - }).join("")} + }) + .join("")} </div> </div> `; - }) - return output; - } - - /********************/ - /* Options */ - /********************/ - setOptions(options) { - Object.entries(options).forEach(([option, value]) => { - if (value === false) { - this.shadow.children[0].classList.add(option.replace("show", "hide")); - } - }); - } - - - /********************/ - /* Fonctions d'aide */ - /********************/ - URL(href, content) { - if (this.config.showURL) { - return `<a href=${href}>${content}</a>`; - } else { - return content; - } - } - civilite(txt) { - switch (txt) { - case "M": return "M."; - case "F": return "Mme"; - default: return ""; - } - } - - ISOToDate(ISO) { - return ISO.split("-").reverse().join("/"); - } - + }); + return output; + } + + /********************/ + /* Options */ + /********************/ + setOptions(options) { + Object.entries(options).forEach(([option, value]) => { + if (value === false) { + this.shadow.children[0].classList.add(option.replace("show", "hide")); + } + }); + } + + /********************/ + /* Fonctions d'aide */ + /********************/ + URL(href, content) { + if (this.config.showURL) { + return `<a href=${href}>${content}</a>`; + } else { + return content; + } + } + civilite(txt) { + switch (txt) { + case "M": + return "M."; + case "F": + return "Mme"; + default: + return ""; + } + } + + ISOToDate(ISO) { + return ISO.split("-").reverse().join("/"); + } } -customElements.define('releve-but', releveBUT); +customElements.define("releve-but", releveBUT); diff --git a/app/templates/sidebar.j2 b/app/templates/sidebar.j2 index b579a1f03275c9bbe64ed2e3f79e3af187eab829..19c2199ff8b6214e2832a1cd23674ad8ab922e85 100755 --- a/app/templates/sidebar.j2 +++ b/app/templates/sidebar.j2 @@ -55,7 +55,7 @@ <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'] }}">(1/2 j.) + au {{ sco.etud_cur_sem['date_fin'] }}">({{sco.prefs["assi_metrique"]}}) <br />{{sco.nbabsjust}} J., {{sco.nbabsnj}} N.J.</span> {% endif %} <ul> diff --git a/app/views/__init__.py b/app/views/__init__.py index 89d3418b792db20c24fefc8a974cd84ce99edfd2..a0edb6ba83cae35aff8b876262f7a6e79c4d6493 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -72,10 +72,13 @@ class ScoData: ins = self.etud.inscription_courante() if ins: self.etud_cur_sem = ins.formsemestre - self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval( + self.nbabs, self.nbabsjust = sco_abs.get_assiduites_count_in_interval( etud.id, self.etud_cur_sem.date_debut.isoformat(), self.etud_cur_sem.date_fin.isoformat(), + scu.translate_assiduites_metric( + sco_preferences.get_preference("assi_metrique") + ), ) self.nbabsnj = self.nbabs - self.nbabsjust else: