diff --git a/app/api/assiduites.py b/app/api/assiduites.py index ec47f518..295fd2ab 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -27,7 +27,7 @@ from app.models import ( Justificatif, ) from flask_sqlalchemy.query import Query -from app.models.assiduites import get_assiduites_justif +from app.models.assiduites import get_assiduites_justif, get_justifs_from_date from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @@ -559,6 +559,7 @@ def _create_singular( data: dict, etud: Identite, ) -> tuple[int, object]: + """TODO: documenter""" errors: list[str] = [] # -- vérifications de l'objet json -- @@ -601,9 +602,12 @@ def _create_singular( moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None - if moduleimpl_id not in [False, None]: + if moduleimpl_id not in [False, None, "", "-1"]: if moduleimpl_id != "autre": - moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + try: + moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + except ValueError: + moduleimpl = None if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: @@ -725,7 +729,6 @@ def assiduite_edit(assiduite_id: int): assiduite_unique.etudiant.id, msg=f"assiduite: modif {assiduite_unique}", ) - db.session.add(assiduite_unique) db.session.commit() scass.simple_invalidate_cache(assiduite_unique.to_dict()) @@ -810,7 +813,7 @@ def _edit_singular(assiduite_unique, data): moduleimpl: ModuleImpl = None if moduleimpl_id is not False: - if moduleimpl_id is not None: + if moduleimpl_id not in [None, "", "-1"]: if moduleimpl_id == "autre": assiduite_unique.moduleimpl_id = None external_data = ( @@ -823,7 +826,13 @@ def _edit_singular(assiduite_unique, data): assiduite_unique.external_data = external_data else: - moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + try: + moduleimpl = ModuleImpl.query.filter_by( + id=int(moduleimpl_id) + ).first() + except ValueError: + moduleimpl = None + if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: @@ -834,20 +843,28 @@ def _edit_singular(assiduite_unique, data): else: assiduite_unique.moduleimpl_id = moduleimpl_id else: - assiduite_unique.moduleimpl_id = moduleimpl_id + assiduite_unique.moduleimpl_id = None # Cas 3 : desc desc = data.get("desc", False) if desc is not False: - assiduite_unique.desc = desc + assiduite_unique.description = desc # Cas 4 : est_just - est_just = data.get("est_just") - if est_just is not None: - if not isinstance(est_just, bool): - errors.append("param 'est_just' : booléen non reconnu") - else: - assiduite_unique.est_just = est_just + if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: + assiduite_unique.est_just = False + else: + assiduite_unique.est_just = ( + len( + get_justifs_from_date( + assiduite_unique.etudiant.id, + assiduite_unique.date_debut, + assiduite_unique.date_fin, + valid=True, + ) + ) + > 0 + ) if errors: err: str = ", ".join(errors) @@ -1015,6 +1032,19 @@ def _filter_manager(requested, assiduites_query: Query) -> Query: if user_id is not False: assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id) + order = requested.args.get("order", None) + if order is not None: + assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc()) + + courant = requested.args.get("courant", None) + if courant is not None: + annee: int = scu.annee_scolaire() + + assiduites_query: Query = assiduites_query.filter( + Assiduite.date_debut >= scu.date_debut_anne_scolaire(annee), + Assiduite.date_fin <= scu.date_fin_anne_scolaire(annee), + ) + return assiduites_query diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 572f77ca..53f2ed3e 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -359,7 +359,7 @@ def bulletin( with_img_signatures_pdf: bool = True, ): """ - Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné + Retourne le bulletin d'un étudiant dans un formsemestre. formsemestre_id : l'id d'un formsemestre code_type : "etudid", "nip" ou "ine" @@ -376,7 +376,7 @@ def bulletin( formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() if g.scodoc_dept and dept.acronym != g.scodoc_dept: - return json_error(404, "formsemestre inexistant", as_response=True) + return json_error(404, "formsemestre inexistant") app.set_sco_dept(dept.acronym) if code_type == "nip": @@ -399,7 +399,7 @@ def bulletin( formsemestre, etud, version=version, - format="pdf", + fmt="pdf", with_img_signatures_pdf=with_img_signatures_pdf, ) return pdf_response diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 0fd59b97..a85685f7 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -8,8 +8,9 @@ from datetime import datetime from flask_json import as_json -from flask import g, jsonify, request +from flask import g, request from flask_login import login_required, current_user +from flask_sqlalchemy.query import Query import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu @@ -18,7 +19,13 @@ from app.api import api_bp as bp from app.api import api_web_bp from app.api import get_model_api_object, tools from app.decorators import permission_required, scodoc -from app.models import Identite, Justificatif, Departement, FormSemestre +from app.models import ( + Identite, + Justificatif, + Departement, + FormSemestre, + FormSemestreInscription, +) from app.models.assiduites import ( compute_assiduites_justified, ) @@ -26,7 +33,7 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error -from flask_sqlalchemy.query import Query +from app.scodoc.sco_groups import get_group_members # Partie Modèle @@ -130,6 +137,8 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal @api_web_bp.route( "/justificatifs/dept//query", defaults={"with_query": True} ) +@bp.route("/justificatifs/dept/", defaults={"with_query": False}) +@bp.route("/justificatifs/dept//query", defaults={"with_query": True}) @login_required @scodoc @as_json @@ -143,9 +152,77 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): if with_query: justificatifs_query = _filter_manager(request, justificatifs_query) + data_set: list[dict] = [] - for just in justificatifs_query.all(): - data = just.to_dict(format_api=True) + for just in justificatifs_query: + data_set.append(_set_sems_and_groupe(just)) + + return data_set + + +def _set_sems_and_groupe(justi: Justificatif) -> dict: + from app.scodoc.sco_groups import get_etud_groups + + data = justi.to_dict(format_api=True) + + formsemestre: FormSemestre = ( + FormSemestre.query.join( + FormSemestreInscription, + FormSemestre.id == FormSemestreInscription.formsemestre_id, + ) + .filter( + justi.date_debut <= FormSemestre.date_fin, + justi.date_fin >= FormSemestre.date_debut, + FormSemestreInscription.etudid == justi.etudid, + ) + .first() + ) + if formsemestre: + data["formsemestre"] = { + "id": formsemestre.id, + "title": formsemestre.session_id(), + } + + return data + + +@bp.route( + "/justificatifs/formsemestre/", defaults={"with_query": False} +) +@api_web_bp.route( + "/justificatifs/formsemestre/", defaults={"with_query": False} +) +@bp.route( + "/justificatifs/formsemestre//query", + defaults={"with_query": True}, +) +@api_web_bp.route( + "/justificatifs/formsemestre//query", + defaults={"with_query": True}, +) +@login_required +@scodoc +@as_json +@permission_required(Permission.ScoView) +def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): + """Retourne tous les justificatifs du formsemestre""" + formsemestre: FormSemestre = None + formsemestre_id = int(formsemestre_id) + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + + if formsemestre is None: + return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") + + justificatifs_query = scass.filter_by_formsemestre( + Justificatif.query, Justificatif, formsemestre + ) + + if with_query: + justificatifs_query = _filter_manager(request, justificatifs_query) + + data_set: list[dict] = [] + for justi in justificatifs_query.all(): + data = justi.to_dict(format_api=True) data_set.append(data) return data_set @@ -380,7 +457,7 @@ def justif_edit(justif_id: int): "après": compute_assiduites_justified( justificatif_unique.etudid, [justificatif_unique], - False, + True, ), } } @@ -436,7 +513,7 @@ def _delete_singular(justif_id: int, database): if archive_name is not None: archiver: JustificatifArchiver = JustificatifArchiver() try: - archiver.delete_justificatif(justificatif_unique.etudid, archive_name) + archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) except ValueError: pass @@ -481,7 +558,7 @@ def justif_import(justif_id: int = None): try: fname: str archive_name, fname = archiver.save_justificatif( - etudid=justificatif_unique.etudid, + justificatif_unique.etudiant, filename=file.filename, data=file.stream.read(), archive_name=archive_name, @@ -512,7 +589,7 @@ def justif_export(justif_id: int = None, filename: str = None): if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificaitf = query.first_or_404() + justificatif_unique: Justificatif = query.first_or_404() archive_name: str = justificatif_unique.fichier if archive_name is None: @@ -522,7 +599,7 @@ def justif_export(justif_id: int = None, filename: str = None): try: return archiver.get_justificatif_file( - archive_name, justificatif_unique.etudid, filename + archive_name, justificatif_unique.etudiant, filename ) except ScoValueError as err: return json_error(404, err.args[0]) @@ -564,10 +641,10 @@ def justif_remove(justif_id: int = None): if remove is None or remove not in ("all", "list"): return json_error(404, "param 'remove': Valeur invalide") archiver: JustificatifArchiver = JustificatifArchiver() - etudid: int = justificatif_unique.etudid + etud = justificatif_unique.etudiant try: if remove == "all": - archiver.delete_justificatif(etudid=etudid, archive_name=archive_name) + archiver.delete_justificatif(etud, archive_name=archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -575,13 +652,13 @@ def justif_remove(justif_id: int = None): else: for fname in data.get("filenames", []): archiver.delete_justificatif( - etudid=etudid, + etud, archive_name=archive_name, filename=fname, ) - if len(archiver.list_justificatifs(archive_name, etudid)) == 0: - archiver.delete_justificatif(etudid, archive_name) + if len(archiver.list_justificatifs(archive_name, etud)) == 0: + archiver.delete_justificatif(etud, archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -616,16 +693,16 @@ def justif_list(justif_id: int = None): archiver: JustificatifArchiver = JustificatifArchiver() if archive_name is not None: filenames = archiver.list_justificatifs( - archive_name, justificatif_unique.etudid + archive_name, justificatif_unique.etudiant ) retour = {"total": len(filenames), "filenames": []} - for fi in filenames: - if int(fi[1]) == current_user.id or current_user.has_permission( + for filename in filenames: + if int(filename[1]) == current_user.id or current_user.has_permission( Permission.ScoJustifView ): - retour["filenames"].append(fi[0]) + retour["filenames"].append(filename[0]) return retour @@ -688,12 +765,41 @@ def _filter_manager(requested, justificatifs_query): # cas 5 : formsemestre_id formsemestre_id = requested.args.get("formsemestre_id") - if formsemestre_id is not None: + if formsemestre_id not in [None, "", -1]: formsemestre: FormSemestre = None - formsemestre_id = int(formsemestre_id) - formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - justificatifs_query = scass.filter_by_formsemestre( - justificatifs_query, Justificatif, formsemestre + try: + formsemestre_id = int(formsemestre_id) + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + justificatifs_query = scass.filter_by_formsemestre( + justificatifs_query, Justificatif, formsemestre + ) + except ValueError: + formsemestre = None + + order = requested.args.get("order", None) + if order is not None: + justificatifs_query: Query = justificatifs_query.order_by( + Justificatif.date_debut.desc() ) + courant = requested.args.get("courant", None) + if courant is not None: + annee: int = scu.annee_scolaire() + + justificatifs_query: Query = justificatifs_query.filter( + Justificatif.date_debut >= scu.date_debut_anne_scolaire(annee), + Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee), + ) + + group_id = requested.args.get("group_id", None) + if group_id is not None: + try: + group_id = int(group_id) + etudids: list[int] = [etu["etudid"] for etu in get_group_members(group_id)] + justificatifs_query = justificatifs_query.filter( + Justificatif.etudid.in_(etudids) + ) + except ValueError: + group_id = None + return justificatifs_query diff --git a/app/api/moduleimpl.py b/app/api/moduleimpl.py index b8e87c05..556f3d07 100644 --- a/app/api/moduleimpl.py +++ b/app/api/moduleimpl.py @@ -67,3 +67,28 @@ def moduleimpl(moduleimpl_id: int): query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) modimpl: ModuleImpl = query.first_or_404() return modimpl.to_dict(convert_objects=True) + + +@bp.route("/moduleimpl//inscriptions") +@api_web_bp.route("/moduleimpl//inscriptions") +@login_required +@scodoc +@permission_required(Permission.ScoView) +@as_json +def moduleimpl_inscriptions(moduleimpl_id: int): + """Liste des inscriptions à ce moduleimpl + Exemple de résultat : + [ + { + "id": 1, + "etudid": 666, + "moduleimpl_id": 1234, + }, + ... + ] + """ + query = ModuleImpl.query.filter_by(id=moduleimpl_id) + if g.scodoc_dept: + query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) + modimpl: ModuleImpl = query.first_or_404() + return [i.to_dict() for i in modimpl.inscriptions] diff --git a/app/auth/models.py b/app/auth/models.py index a293685e..073f687e 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -306,6 +306,13 @@ class User(UserMixin, db.Model): role, dept = UserRole.role_dept_from_string(r_d) self.add_role(role, dept) + # Set cas_id using regexp if configured: + exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp") + if exp and self.email_institutionnel: + cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel) + if cas_id is not None: + self.cas_id = cas_id + def get_token(self, expires_in=3600): "Un jeton pour cet user. Stocké en base, non commité." now = datetime.utcnow() diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 4cf2e88c..494fe090 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -512,10 +512,10 @@ class BulletinBUT: d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id) # --- Decision Jury - infos, dpv = sco_bulletins.etud_descr_situation_semestre( + infos, _ = sco_bulletins.etud_descr_situation_semestre( etud.id, self.res.formsemestre, - format="html", + fmt="html", show_date_inscr=self.prefs["bul_show_date_inscr"], show_decisions=self.prefs["bul_show_decision"], show_uevalid=self.prefs["bul_show_uevalid"], diff --git a/app/but/bulletin_but_court.py b/app/but/bulletin_but_court.py index 922e26de..f88b319f 100644 --- a/app/but/bulletin_but_court.py +++ b/app/but/bulletin_but_court.py @@ -69,13 +69,13 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"): if fmt == "pdf": bul: dict = bulletins_sem.bulletin_etud_complet(etud) else: # la même chose avec un peu moins d'infos - bul: dict = bulletins_sem.bulletin_etud(etud) + bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True) decision_ues = ( {x["acronyme"]: x for x in bul["semestre"]["decision_ue"]} if "semestre" in bul and "decision_ue" in bul["semestre"] else {} ) - if not "ues" in bul: + if "ues" not in bul: raise ScoValueError("Aucune UE à afficher") cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation) refcomp = formsemestre.formation.referentiel_competence diff --git a/app/but/bulletin_but_court_pdf.py b/app/but/bulletin_but_court_pdf.py index 9642de83..9446d847 100644 --- a/app/but/bulletin_but_court_pdf.py +++ b/app/but/bulletin_but_court_pdf.py @@ -50,7 +50,7 @@ def make_bulletin_but_court_pdf( try: PDFLOCK.acquire() bul_generator = BulletinGeneratorBUTCourt(**locals()) - bul_pdf = bul_generator.generate(format="pdf") + bul_pdf = bul_generator.generate(fmt="pdf") finally: PDFLOCK.release() return bul_pdf @@ -499,14 +499,15 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): def boite_decisions_jury(self): """La boite en bas à droite avec jury""" txt = f"""ECTS acquis en BUT : {self.ects_total:g}
""" - if self.bul["semestre"]["decision_annee"]: + if self.bul["semestre"].get("decision_annee", None): txt += f""" - Jury tenu le { + Décision saisie le { datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y") - }, année BUT {self.bul["semestre"]["decision_annee"]["code"]}. + }, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]} + {self.bul["semestre"]["decision_annee"]["code"]}.
""" - if self.bul["semestre"]["autorisation_inscription"]: + if self.bul["semestre"].get("autorisation_inscription", None): txt += ( "
Autorisé à s'inscrire en " + ", ".join( diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index c52c4431..dd3ec687 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -14,7 +14,7 @@ La génération du bulletin PDF suit le chemin suivant: - sco_bulletins_generator.make_formsemestre_bulletin_etud() - instance de BulletinGeneratorStandardBUT -- BulletinGeneratorStandardBUT.generate(format="pdf") +- BulletinGeneratorStandardBUT.generate(fmt="pdf") sco_bulletins_generator.BulletinGenerator.generate() .generate_pdf() .bul_table() (ci-dessous) @@ -24,6 +24,7 @@ from reportlab.lib.colors import blue from reportlab.lib.units import cm, mm from reportlab.platypus import Paragraph, Spacer +from app.models import ScoDocSiteConfig from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc import gen_tables from app.scodoc.codes_cursus import UE_SPORT @@ -48,6 +49,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): - en HTML: une chaine - en PDF: une liste d'objets PLATYPUS (eg instance de Table). """ + if fmt == "pdf" and ScoDocSiteConfig.is_bul_pdf_disabled(): + return [Paragraph("

Export des PDF interdit par l'administrateur

")] tables_infos = [ # ---- TABLE SYNTHESE UES self.but_table_synthese_ues(), @@ -71,7 +74,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): html_class_ignore_default=True, html_with_td_classes=True, ) - table_objects = table.gen(format=fmt) + table_objects = table.gen(fmt=fmt) objects += table_objects # objects += [KeepInFrame(0, 0, table_objects, mode="shrink")] if i != 2: diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 75dade0a..4ab940f7 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -258,7 +258,7 @@ def bulletin_but_xml_compat( infos, dpv = sco_bulletins.etud_descr_situation_semestre( etudid, formsemestre, - format="xml", + fmt="xml", show_uevalid=sco_preferences.get_preference( "bul_show_uevalid", formsemestre_id ), diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 4eb7525f..4309342b 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -61,14 +61,12 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT from datetime import datetime import html import re -from typing import Union import numpy as np from flask import flash, g, url_for from app import db from app import log -from app.but import cursus_but from app.but.cursus_but import EtudCursusBUT from app.but.rcue import RegroupementCoherentUE from app.comp.res_but import ResultatsSemestreBUT @@ -150,7 +148,7 @@ class DecisionsProposees: def __init__( self, etud: Identite = None, - code: Union[str, list[str]] = None, + code: str | list[str] | None = None, explanation="", code_valide=None, include_communs=True, diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index d0933c7a..0b8dfd06 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -94,7 +94,7 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"): }, xls_style_base=xls_style_base, ) - return tab.make_page(format=fmt, javascripts=["js/etud_info.js"], init_qtip=True) + return tab.make_page(fmt=fmt, javascripts=["js/etud_info.js"], init_qtip=True) def pvjury_table_but( diff --git a/app/but/rcue.py b/app/but/rcue.py index b7cf2711..5a5fce3e 100644 --- a/app/but/rcue.py +++ b/app/but/rcue.py @@ -6,7 +6,6 @@ """Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs """ -from typing import Union from flask_sqlalchemy.query import Query from app.comp.res_but import ResultatsSemestreBUT @@ -205,7 +204,7 @@ class RegroupementCoherentUE: self.moy_rcue > codes_cursus.BUT_BARRE_RCUE ) - def code_valide(self) -> Union[ApcValidationRCUE, None]: + def code_valide(self) -> ApcValidationRCUE | None: "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None" validation = self.query_validations().first() if (validation is not None) and ( diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 071d72e8..ea9124ee 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -344,8 +344,12 @@ def compute_ue_moys_classic( pd.Series( [val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index ), - pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index), - pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index), + pd.DataFrame( + columns=[ue.id for ue in ues], index=modimpl_inscr_df.index, dtype=float + ), + pd.DataFrame( + columns=[ue.id for ue in ues], index=modimpl_inscr_df.index, dtype=float + ), ) # Restreint aux modules sélectionnés: sem_matrix = sem_matrix[:, modimpl_mask] @@ -400,6 +404,7 @@ def compute_ue_moys_classic( }, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues], + dtype=float, ) # remplace NaN par zéros dans les moyennes d'UE etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False) @@ -415,6 +420,7 @@ def compute_ue_moys_classic( coefs.sum(axis=2).T, index=modimpl_inscr_df.index, # etudids columns=[ue.id for ue in ues], + dtype=float, ) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etud_moy_gen = np.sum( diff --git a/app/decorators.py b/app/decorators.py index 08aea4f7..d8816d61 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -186,7 +186,10 @@ def scodoc7func(func): arg_names = argspec.args for arg_name in arg_names: # pour chaque arg de la fonction vue # peut produire une KeyError s'il manque un argument attendu: - v = req_args[arg_name] + try: + v = req_args[arg_name] + except KeyError as exc: + raise ScoValueError(f"argument {arg_name} manquant") from exc # try to convert all arguments to INTEGERS # necessary for db ids and boolean values try: diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py index 1c18135d..7f2cdaa2 100644 --- a/app/forms/main/config_assiduites.py +++ b/app/forms/main/config_assiduites.py @@ -76,7 +76,7 @@ class TimeField(StringField): class ConfigAssiduitesForm(FlaskForm): - "Formulaire paramétrage Module Assiduités" + "Formulaire paramétrage Module Assiduité" morning_time = TimeField("Début de la journée") lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)") diff --git a/app/forms/main/config_cas.py b/app/forms/main/config_cas.py index f68aa3cc..7e2b73c5 100644 --- a/app/forms/main/config_cas.py +++ b/app/forms/main/config_cas.py @@ -30,8 +30,17 @@ Formulaire configuration CAS """ from flask_wtf import FlaskForm -from wtforms import BooleanField, SubmitField +from wtforms import BooleanField, SubmitField, ValidationError from wtforms.fields.simple import FileField, StringField +from wtforms.validators import Optional + +from app.models import ScoDocSiteConfig + + +def check_cas_uid_from_mail_regexp(form, field): + "Vérifie la regexp fournie pur l'extraction du CAS id" + if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data): + raise ValidationError("expression régulière invalide") class ConfigCASForm(FlaskForm): @@ -50,7 +59,8 @@ class ConfigCASForm(FlaskForm): ) cas_login_route = StringField( label="Route du login CAS", - description="""ajouté à l'URL du serveur: exemple /cas (si commence par /, part de la racine)""", + description="""ajouté à l'URL du serveur: exemple /cas + (si commence par /, part de la racine)""", default="/cas", ) cas_logout_route = StringField( @@ -70,6 +80,18 @@ class ConfigCASForm(FlaskForm): comptes utilisateurs.""", ) + cas_uid_from_mail_regexp = StringField( + label="Expression pour extraire l'identifiant utilisateur", + description="""regexp python appliquée au mail institutionnel de l'utilisateur, + dont le premier groupe doit donner l'identifiant CAS. + Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte. + Par exemple, (.*)@ indique que le mail sans le domaine (donc toute + la partie avant le @) est l'identifiant. + Pour prendre le mail complet, utiliser (.*). + """, + validators=[Optional(), check_cas_uid_from_mail_regexp], + ) + cas_ssl_verify = BooleanField("Vérification du certificat SSL") cas_ssl_certificate_file = FileField( label="Certificat (PEM)", diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index f9eff362..8c955c9c 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -76,6 +76,7 @@ class ScoDocConfigurationForm(FlaskForm): Attention: si ce champ peut aussi être défini dans chaque département.""", validators=[Optional(), Email()], ) + disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF") submit_scodoc = SubmitField("Valider") cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) @@ -94,6 +95,7 @@ def configuration(): "month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(), "month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(), "email_from_addr": ScoDocSiteConfig.get("email_from_addr"), + "disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(), } ) if request.method == "POST" and ( @@ -139,6 +141,13 @@ def configuration(): ) if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]): flash("Adresse email origine enregistrée") + if ScoDocSiteConfig.disable_bul_pdf( + enabled=form_scodoc.data["disable_bul_pdf"] + ): + flash( + "Exports PDF " + + ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés") + ) return redirect(url_for("scodoc.index")) return render_template( diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 7f5520df..82b6d3dc 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -123,7 +123,7 @@ class Assiduite(db.Model): user_id: int = None, est_just: bool = False, external_data: dict = None, - ) -> object or int: + ) -> "Assiduite": """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes assiduites: Query = etud.assiduites @@ -134,7 +134,10 @@ class Assiduite(db.Model): if not est_just: est_just = ( - len(_get_assiduites_justif(etud.etudid, date_debut, date_fin)) > 0 + len( + get_justifs_from_date(etud.etudid, date_debut, date_fin, valid=True) + ) + > 0 ) if moduleimpl is not None: @@ -153,7 +156,7 @@ class Assiduite(db.Model): external_data=external_data, ) else: - raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl") + raise ScoValueError("L'étudiant n'est pas inscrit au module") else: nouv_assiduite = Assiduite( date_debut=date_debut, @@ -282,7 +285,7 @@ class Justificatif(db.Model): entry_date: datetime = None, user_id: int = None, external_data: dict = None, - ) -> object or int: + ) -> "Justificatif": """Créer un nouveau justificatif pour l'étudiant""" nouv_justificatif = Justificatif( date_debut=date_debut, @@ -310,7 +313,7 @@ def is_period_conflicting( date_debut: datetime, date_fin: datetime, collection: Query, - collection_cls: Assiduite or Justificatif, + collection_cls: Assiduite | Justificatif, ) -> bool: """ Vérifie si une date n'entre pas en collision @@ -350,14 +353,26 @@ def compute_assiduites_justified( if justificatifs is None: justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() + justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE] + assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites_justifiees: list[int] = [] for assi in assiduites: + if assi.etat == EtatAssiduite.PRESENT: + continue + + 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() + if any( assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin - for j in justificatifs + for j in justificatifs + assi_justificatifs ): assi.est_just = True assiduites_justifiees.append(assi.assiduite_id) @@ -371,16 +386,23 @@ def compute_assiduites_justified( def get_assiduites_justif(assiduite_id: int, long: bool): assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) - return _get_assiduites_justif(assi.etudid, assi.date_debut, assi.date_fin, long) + return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long) -def _get_assiduites_justif( - etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False +def get_justifs_from_date( + etudid: int, + date_debut: datetime, + date_fin: datetime, + long: bool = False, + valid: bool = False, ): - justifs: Justificatif = Justificatif.query.filter( + justifs: Query = Justificatif.query.filter( Justificatif.etudid == etudid, Justificatif.date_debut <= date_debut, Justificatif.date_fin >= date_fin, ) + if valid: + justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE) + return [j.justif_id if not long else j.to_dict(True) for j in justifs] diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 6a3939a6..539c1239 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -214,10 +214,12 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: decisions["decision_rcue"] = [] decisions["descr_decisions_rcue"] = "" decisions["descr_decisions_niveaux"] = "" - # --- Année: prend la validation pour l'année scolaire de ce semestre + # --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre + annee_but = (formsemestre.semestre_id + 1) // 2 validation = ApcValidationAnnee.query.filter_by( etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire(), + ordre=annee_but, referentiel_competence_id=formsemestre.formation.referentiel_competence_id, ).first() if validation: diff --git a/app/models/config.py b/app/models/config.py index c436248f..60ce884b 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -5,6 +5,7 @@ import json import urllib.parse +import re from flask import flash from app import current_app, db, log @@ -95,6 +96,7 @@ class ScoDocSiteConfig(db.Model): "enable_entreprises": bool, "month_debut_annee_scolaire": int, "month_debut_periode2": int, + "disable_bul_pdf": bool, # CAS "cas_enable": bool, "cas_server": str, @@ -102,7 +104,8 @@ class ScoDocSiteConfig(db.Model): "cas_logout_route": str, "cas_validate_route": str, "cas_attribute_id": str, - # Assiduités + "cas_uid_from_mail_regexp": str, + # Assiduité "morning_time": str, "lunch_time": str, "afternoon_time": str, @@ -235,6 +238,12 @@ class ScoDocSiteConfig(db.Model): cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first() return cfg is not None and cfg.value + @classmethod + def is_bul_pdf_disabled(cls) -> bool: + """True si on interdit les exports PDF des bulltins""" + cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first() + return cfg is not None and cfg.value + @classmethod def enable_entreprises(cls, enabled=True) -> bool: """Active (ou déactive) le module entreprises. True si changement.""" @@ -251,6 +260,22 @@ class ScoDocSiteConfig(db.Model): return True return False + @classmethod + def disable_bul_pdf(cls, enabled=True) -> bool: + """Interedit (ou autorise) les exports PDF. True si changement.""" + if enabled != ScoDocSiteConfig.is_bul_pdf_disabled(): + cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first() + if cfg is None: + cfg = ScoDocSiteConfig( + name="disable_bul_pdf", value="on" if enabled else "" + ) + else: + cfg.value = "on" if enabled else "" + db.session.add(cfg) + db.session.commit() + return True + return False + @classmethod def get(cls, name: str, default: str = "") -> str: "Get configuration param; empty string or specified default if unset" @@ -360,7 +385,7 @@ class ScoDocSiteConfig(db.Model): cls.set("personalized_links", "") raise ScoValueError( "Attention: liens personnalisés erronés: ils ont été effacés." - ) + ) from exc return [PersonalizedLink(**item) for item in links_dict] @classmethod @@ -372,6 +397,59 @@ class ScoDocSiteConfig(db.Model): data_links = json.dumps(links_dict) cls.set("personalized_links", data_links) + @classmethod + def extract_cas_id(cls, email_addr: str) -> str | None: + "Extract cas_id from maill, using regexp in config. None if not possible." + exp = cls.get("cas_uid_from_mail_regexp") + if not exp or not email_addr: + return None + try: + match = re.search(exp, email_addr) + except re.error: + log("error extracting CAS id from '{email_addr}' using regexp '{exp}'") + return None + if not match: + log("no match extracting CAS id from '{email_addr}' using regexp '{exp}'") + return None + try: + cas_id = match.group(1) + except IndexError: + log( + "no group found extracting CAS id from '{email_addr}' using regexp '{exp}'" + ) + return None + return cas_id + + @classmethod + def cas_uid_from_mail_regexp_is_valid(cls, exp: str) -> bool: + "True si l'expression régulière semble valide" + # check that it compiles + try: + pattern = re.compile(exp) + except re.error: + return False + # and returns at least one group on a simple cannonical address + match = pattern.search("emmanuel@exemple.fr") + return len(match.groups()) > 0 + + @classmethod + def assi_get_rounded_time(cls, label: str, default: str) -> float: + "Donne l'heure stockée dans la config globale sous label, en float arrondi au quart d'heure" + return _round_time_str_to_quarter(cls.get(label, default)) + + +def _round_time_str_to_quarter(string: str) -> float: + """Prend une heure iso '12:20:23', et la converti en un nombre d'heures + en arrondissant au quart d'heure: (les secondes sont ignorées) + "12:20:00" -> 12.25 + "12:29:00" -> 12.25 + "12:30:00" -> 12.5 + """ + parts = [*map(float, string.split(":"))] + hour = parts[0] + minutes = round(parts[1] / 60 * 4) / 4 + return hour + minutes + class PersonalizedLink: def __init__(self, title: str = "", url: str = "", with_args: bool = False): diff --git a/app/models/etudiants.py b/app/models/etudiants.py index e00f8465..a451526f 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -74,9 +74,11 @@ class Identite(db.Model): ) # Relations avec les assiduites et les justificatifs - assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic") + assiduites = db.relationship( + "Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete" + ) justificatifs = db.relationship( - "Justificatif", back_populates="etudiant", lazy="dynamic" + "Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete" ) def __repr__(self): diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 73659aa3..63fac264 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -536,7 +536,9 @@ def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict): raise ScoValueError("invalid note_max value (must be positive or null)") data["note_max"] = note_max # --- coefficient - coef = data.get("coefficient", 1.0) or 1.0 + coef = data.get("coefficient", None) + if coef is None: + coef = 1.0 try: coef = float(coef) except ValueError as exc: diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 20a1cf35..3f8f61f1 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -18,6 +18,7 @@ from flask_login import current_user from flask import flash, g, url_for from sqlalchemy.sql import text +from sqlalchemy import func import app.scodoc.sco_utils as scu from app import db, log @@ -138,6 +139,7 @@ class FormSemestre(db.Model): secondary="notes_formsemestre_responsables", lazy=True, backref=db.backref("formsemestres", lazy=True), + order_by=func.upper(User.nom), ) partitions = db.relationship( "Partition", @@ -195,6 +197,7 @@ class FormSemestre(db.Model): """ d = dict(self.__dict__) d.pop("_sa_instance_state", None) + d.pop("groups_auto_assignment_data", None) # ScoDoc7 output_formators: (backward compat) d["formsemestre_id"] = self.id d["titre_num"] = self.titre_num() @@ -226,6 +229,7 @@ class FormSemestre(db.Model): """ d = dict(self.__dict__) d.pop("_sa_instance_state", None) + d.pop("groups_auto_assignment_data", None) d["annee_scolaire"] = self.annee_scolaire() if self.date_debut: d["date_debut"] = self.date_debut.strftime("%d/%m/%Y") @@ -767,6 +771,15 @@ class FormSemestre(db.Model): etuds.sort(key=lambda e: e.sort_key) return etuds + def get_partitions_list(self, with_default=True) -> list[Partition]: + """Liste des partitions pour ce semestre (list of dicts), + triées par numéro, avec la partition par défaut en fin de liste. + """ + partitions = [p for p in self.partitions if p.partition_name is not None] + if with_default: + partitions += [p for p in self.partitions if p.partition_name is None] + return partitions + @cached_property def etudids_actifs(self) -> set: "Set des etudids inscrits non démissionnaires et non défaillants" diff --git a/app/models/groups.py b/app/models/groups.py index b8fb5f46..a4a5792f 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -12,6 +12,7 @@ from sqlalchemy.exc import IntegrityError from app import db, log from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN +from app.models.etudiants import Identite from app.scodoc import sco_cache from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import AccessDenied, ScoValueError @@ -50,7 +51,7 @@ class Partition(db.Model): backref=db.backref("partition", lazy=True), lazy="dynamic", cascade="all, delete-orphan", - order_by="GroupDescr.numero", + order_by="GroupDescr.numero, GroupDescr.group_name", ) def __init__(self, **kwargs): @@ -240,6 +241,21 @@ class GroupDescr(db.Model): d["partition"] = self.partition.to_dict(with_groups=False) return d + def get_nb_inscrits(self) -> int: + """Nombre inscrits à ce group et au formsemestre. + C'est nécessaire car lors d'une désinscription, on conserve l'appartenance + aux groupes pour facilier une éventuelle ré-inscription. + """ + from app.models.formsemestre import FormSemestreInscription + + return ( + Identite.query.join(group_membership) + .filter_by(group_id=self.id) + .join(FormSemestreInscription) + .filter_by(formsemestre_id=self.partition.formsemestre.id) + .count() + ) + @classmethod def check_name( cls, partition: "Partition", group_name: str, existing=False, default=False diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index a44a4b5f..27df7f48 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -219,6 +219,14 @@ class ModuleImplInscription(db.Model): backref=db.backref("inscriptions", cascade="all, delete-orphan"), ) + def to_dict(self) -> dict: + "dict repr." + return { + "id": self.id, + "etudid": self.etudid, + "moduleimpl_id": self.moduleimpl_id, + } + @classmethod def etud_modimpls_in_ue( cls, formsemestre_id: int, etudid: int, ue_id: int diff --git a/app/models/modules.py b/app/models/modules.py index 9fafe5cb..b4aa00ad 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -182,8 +182,8 @@ class Module(db.Model): Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée. """ if self.formation.has_locked_sems(self.ue.semestre_idx): - current_app.logguer.info( - f"set_ue_coef_dict: locked formation, ignoring request" + current_app.logger.info( + "set_ue_coef_dict: locked formation, ignoring request" ) raise ScoValueError("Formation verrouillée") changed = False @@ -213,8 +213,8 @@ class Module(db.Model): def update_ue_coef_dict(self, ue_coef_dict: dict): """update coefs vers UE (ajoute aux existants)""" if self.formation.has_locked_sems(self.ue.semestre_idx): - current_app.logguer.info( - f"update_ue_coef_dict: locked formation, ignoring request" + current_app.logger.info( + "update_ue_coef_dict: locked formation, ignoring request" ) raise ScoValueError("Formation verrouillée") current = self.get_ue_coef_dict() @@ -232,7 +232,7 @@ class Module(db.Model): def delete_ue_coef(self, ue): """delete coef""" if self.formation.has_locked_sems(self.ue.semestre_idx): - current_app.logguer.info( + current_app.logger.info( "delete_ue_coef: locked formation, ignoring request" ) raise ScoValueError("Formation verrouillée") diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 0920b81f..8fcda8fb 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -297,23 +297,23 @@ class GenTable: "list of titles" return [self.titles.get(cid, "") for cid in self.columns_ids] - def gen(self, format="html", columns_ids=None): + def gen(self, fmt="html", columns_ids=None): """Build representation of the table in the specified format. See make_page() for more sophisticated output. """ - if format == "html": + if fmt == "html": return self.html() - elif format == "xls" or format == "xlsx": + elif fmt == "xls" or fmt == "xlsx": return self.excel() - elif format == "text" or format == "csv": + elif fmt == "text" or fmt == "csv": return self.text() - elif format == "pdf": + elif fmt == "pdf": return self.pdf() - elif format == "xml": + elif fmt == "xml": return self.xml() - elif format == "json": + elif fmt == "json": return self.json() - raise ValueError(f"GenTable: invalid format: {format}") + raise ValueError(f"GenTable: invalid format: {fmt}") def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""): "row is a dict, returns a string ..." @@ -477,15 +477,13 @@ class GenTable: H.append('') if self.xls_link: H.append( - ' %s' - % (self.base_url, scu.ICON_XLS) + ' %s' % (self.base_url, scu.ICON_XLS) ) if self.xls_link and self.pdf_link: H.append(" ") if self.pdf_link: H.append( - ' %s' - % (self.base_url, scu.ICON_PDF) + ' %s' % (self.base_url, scu.ICON_PDF) ) H.append("") H.append("

") @@ -653,7 +651,7 @@ class GenTable: def make_page( self, title="", - format="html", + fmt="html", page_title="", filename=None, javascripts=[], @@ -670,7 +668,7 @@ class GenTable: filename = self.filename page_title = page_title or self.page_title html_title = self.html_title or title - if format == "html": + if fmt == "html": H = [] if with_html_headers: H.append( @@ -687,7 +685,7 @@ class GenTable: if with_html_headers: H.append(html_sco_header.sco_footer()) return "\n".join(H) - elif format == "pdf": + elif fmt == "pdf": pdf_objs = self.pdf() pdf_doc = sco_pdf.pdf_basic_page( pdf_objs, title=title, preferences=self.preferences @@ -701,7 +699,7 @@ class GenTable: ) else: return pdf_doc - elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx + elif fmt == "xls" or fmt == "xlsx": # dans les 2 cas retourne du xlsx xls = self.excel() if publish: return scu.send_file( @@ -712,9 +710,9 @@ class GenTable: ) else: return xls - elif format == "text": + elif fmt == "text": return self.text() - elif format == "csv": + elif fmt == "csv": return scu.send_file( self.text(), filename, @@ -722,14 +720,14 @@ class GenTable: mime=scu.CSV_MIMETYPE, attached=True, ) - elif format == "xml": + elif fmt == "xml": xml = self.xml() if publish: return scu.send_file( xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE ) return xml - elif format == "json": + elif fmt == "json": js = self.json() if publish: return scu.send_file( @@ -737,7 +735,7 @@ class GenTable: ) return js else: - log("make_page: format=%s" % format) + log(f"make_page: format={fmt}") raise ValueError("_make_page: invalid format") @@ -771,19 +769,18 @@ if __name__ == "__main__": columns_ids=("nom", "age"), ) print("--- HTML:") - print(table.gen(format="html")) + print(table.gen(fmt="html")) print("\n--- XML:") - print(table.gen(format="xml")) + print(table.gen(fmt="xml")) print("\n--- JSON:") - print(table.gen(format="json")) + print(table.gen(fmt="json")) # Test pdf: import io - from reportlab.platypus import KeepInFrame - from app.scodoc import sco_preferences, sco_pdf + from app.scodoc import sco_preferences preferences = sco_preferences.SemPreferences() table.preferences = preferences - objects = table.gen(format="pdf") + objects = table.gen(fmt="pdf") objects = [KeepInFrame(0, 0, objects, mode="shrink")] doc = io.BytesIO() document = sco_pdf.BaseDocTemplate(doc) @@ -796,6 +793,6 @@ if __name__ == "__main__": data = doc.getvalue() with open("/tmp/gen_table.pdf", "wb") as f: f.write(data) - p = table.make_page(format="pdf") + p = table.make_page(fmt="pdf") with open("toto.pdf", "wb") as f: f.write(p) diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 6e483df3..1f8ede5d 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -58,7 +58,7 @@ def sidebar_common(): ] if current_user.has_permission(Permission.ScoAbsChange): H.append( - f""" Assiduités
""" + f""" Assiduité
""" ) if current_user.has_permission( Permission.ScoUsersAdmin diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index c6a646ee..789509c3 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -47,7 +47,6 @@ nommé _description.txt qui est une description (humaine, format libre) de l'archive. """ -from typing import Union import datetime import glob import json @@ -81,7 +80,7 @@ from app.scodoc import sco_pv_pdf from app.scodoc.sco_exceptions import ScoValueError -class BaseArchiver(object): +class BaseArchiver: def __init__(self, archive_type=""): self.archive_type = archive_type self.initialized = False @@ -92,14 +91,17 @@ class BaseArchiver(object): "set dept" self.dept_id = dept_id - def initialize(self): + def initialize(self, dept_id: int = None): + """Fixe le département et initialise les répertoires au besoin.""" + # Set departement (à chaque fois car peut changer d'une utilisation à l'autre) + self.dept_id = getattr(g, "scodoc_dept_id") if dept_id is None else dept_id if self.initialized: return dirs = [Config.SCODOC_VAR_DIR, "archives"] if self.archive_type: dirs.append(self.archive_type) - self.root = os.path.join(*dirs) + self.root = os.path.join(*dirs) # /opt/scodoc-data/archives/ log("initialized archiver, path=" + self.root) path = dirs[0] for directory in dirs[1:]: @@ -112,15 +114,13 @@ class BaseArchiver(object): finally: scu.GSL.release() self.initialized = True - if self.dept_id is None: - self.dept_id = getattr(g, "scodoc_dept_id") - def get_obj_dir(self, oid: int): + def get_obj_dir(self, oid: int, dept_id: int = None): """ :return: path to directory of archives for this object (eg formsemestre_id or etudid). If directory does not yet exist, create it. """ - self.initialize() + self.initialize(dept_id) dept_dir = os.path.join(self.root, str(self.dept_id)) try: scu.GSL.acquire() @@ -141,21 +141,21 @@ class BaseArchiver(object): scu.GSL.release() return obj_dir - def list_oids(self): + def list_oids(self, dept_id: int = None): """ :return: list of archive oids """ - self.initialize() + self.initialize(dept_id) base = os.path.join(self.root, str(self.dept_id)) + os.path.sep dirs = glob.glob(base + "*") return [os.path.split(x)[1] for x in dirs] - def list_obj_archives(self, oid: int): + def list_obj_archives(self, oid: int, dept_id: int = None): """Returns :return: list of archive identifiers for this object (paths to non empty dirs) """ - self.initialize() - base = self.get_obj_dir(oid) + os.path.sep + self.initialize(dept_id) + base = self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep dirs = glob.glob( base + "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]" @@ -165,9 +165,9 @@ class BaseArchiver(object): dirs.sort() return dirs - def delete_archive(self, archive_id: str): + def delete_archive(self, archive_id: str, dept_id: int = None): """Delete (forever) this archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() shutil.rmtree(archive_id, ignore_errors=True) @@ -180,9 +180,9 @@ class BaseArchiver(object): *[int(x) for x in os.path.split(archive_id)[1].split("-")] ) - def list_archive(self, archive_id: str) -> str: + def list_archive(self, archive_id: str, dept_id: int = None) -> str: """Return list of filenames (without path) in archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() files = os.listdir(archive_id) @@ -201,12 +201,12 @@ class BaseArchiver(object): "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name ) - def get_id_from_name(self, oid, archive_name: str): + def get_id_from_name(self, oid, archive_name: str, dept_id: int = None): """returns archive id (check that name is valid)""" - self.initialize() + self.initialize(dept_id) if not self.is_valid_archive_name(archive_name): raise ScoValueError(f"Archive {archive_name} introuvable") - archive_id = os.path.join(self.get_obj_dir(oid), archive_name) + archive_id = os.path.join(self.get_obj_dir(oid, dept_id=dept_id), archive_name) if not os.path.isdir(archive_id): log( f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}" @@ -214,9 +214,9 @@ class BaseArchiver(object): raise ScoValueError(f"Archive {archive_name} introuvable") return archive_id - def get_archive_description(self, archive_id: str) -> str: + def get_archive_description(self, archive_id: str, dept_id: int = None) -> str: """Return description of archive""" - self.initialize() + self.initialize(dept_id) filename = os.path.join(archive_id, "_description.txt") try: with open(filename, encoding=scu.SCO_ENCODING) as f: @@ -229,11 +229,11 @@ class BaseArchiver(object): return descr - def create_obj_archive(self, oid: int, description: str): + def create_obj_archive(self, oid: int, description: str, dept_id: int = None): """Creates a new archive for this object and returns its id.""" # id suffixé par YYYY-MM-DD-hh-mm-ss archive_id = ( - self.get_obj_dir(oid) + self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep + "-".join([f"{x:02d}" for x in time.localtime()[:6]]) ) @@ -248,7 +248,13 @@ class BaseArchiver(object): self.store(archive_id, "_description.txt", description) return archive_id - def store(self, archive_id: str, filename: str, data: Union[str, bytes]): + def store( + self, + archive_id: str, + filename: str, + data: str | bytes, + dept_id: int = None, + ): """Store data in archive, under given filename. Filename may be modified (sanitized): return used filename The file is created or replaced. @@ -256,7 +262,7 @@ class BaseArchiver(object): """ if isinstance(data, str): data = data.encode(scu.SCO_ENCODING) - self.initialize() + self.initialize(dept_id) filename = scu.sanitize_filename(filename) log(f"storing {filename} ({len(data)} bytes) in {archive_id}") try: @@ -264,27 +270,36 @@ class BaseArchiver(object): fname = os.path.join(archive_id, filename) with open(fname, "wb") as f: f.write(data) + except FileNotFoundError as exc: + raise ScoValueError( + f"Erreur stockage archive (dossier inexistant, chemin {fname})" + ) from exc finally: scu.GSL.release() return filename - def get(self, archive_id: str, filename: str): + def get(self, archive_id: str, filename: str, dept_id: int = None): """Retreive data""" - self.initialize() + self.initialize(dept_id) if not scu.is_valid_filename(filename): log(f"""Archiver.get: invalid filename '{filename}'""") raise ScoValueError("archive introuvable (déjà supprimée ?)") fname = os.path.join(archive_id, filename) log(f"reading archive file {fname}") - with open(fname, "rb") as f: - data = f.read() + try: + with open(fname, "rb") as f: + data = f.read() + except FileNotFoundError as exc: + raise ScoValueError( + f"Erreur lecture archive (inexistant, chemin {fname})" + ) from exc return data - def get_archived_file(self, oid, archive_name, filename): + def get_archived_file(self, oid, archive_name, filename, dept_id: int = None): """Recupère les donnees du fichier indiqué et envoie au client. Returns: Response """ - archive_id = self.get_id_from_name(oid, archive_name) + archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id) data = self.get(archive_id, filename) mime = mimetypes.guess_type(filename)[0] if mime is None: @@ -298,7 +313,7 @@ class SemsArchiver(BaseArchiver): BaseArchiver.__init__(self, archive_type="") -PVArchive = SemsArchiver() +PV_ARCHIVER = SemsArchiver() # ---------------------------------------------------------------------------- @@ -332,8 +347,10 @@ def do_formsemestre_archive( formsemestre = FormSemestre.get_formsemestre(formsemestre_id) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem_archive_id = formsemestre_id - archive_id = PVArchive.create_obj_archive(sem_archive_id, description) - date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") + archive_id = PV_ARCHIVER.create_obj_archive( + sem_archive_id, description, formsemestre.dept_id + ) + date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") if not group_ids: # tous les inscrits du semestre @@ -347,7 +364,12 @@ def do_formsemestre_archive( # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True) if data: - PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) + PV_ARCHIVER.store( + archive_id, + "Tableau_moyennes" + scu.XLSX_SUFFIX, + data, + dept_id=formsemestre.dept_id, + ) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) table_html, _, _ = gen_formsemestre_recapcomplet_html_table( formsemestre, res, include_evaluations=True @@ -367,33 +389,43 @@ def do_formsemestre_archive( html_sco_header.sco_footer(), ] ) - PVArchive.store(archive_id, "Tableau_moyennes.html", data) + PV_ARCHIVER.store( + archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id + ) # Bulletins en JSON data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) if data: - PVArchive.store(archive_id, "Bulletins.json", data_js) + PV_ARCHIVER.store( + archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id + ) # Décisions de jury, en XLS if formsemestre.formation.is_apc(): response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") data = response.get_data() else: # formations classiques data = sco_pv_forms.formsemestre_pvjury( - formsemestre_id, format="xls", publish=False + formsemestre_id, fmt="xls", publish=False ) if data: - PVArchive.store( + PV_ARCHIVER.store( archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data, + dept_id=formsemestre.dept_id, ) # Classeur bulletins (PDF) data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=bul_version ) if data: - PVArchive.store(archive_id, "Bulletins.pdf", data) + PV_ARCHIVER.store( + archive_id, + "Bulletins.pdf", + data, + dept_id=formsemestre.dept_id, + ) # Lettres individuelles (PDF): data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, @@ -403,7 +435,12 @@ def do_formsemestre_archive( signature=signature, ) if data: - PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"CourriersDecisions{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) # PV de jury (PDF): data = sco_pv_pdf.pvjury_pdf( @@ -419,7 +456,12 @@ def do_formsemestre_archive( anonymous=anonymous, ) if data: - PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"PV_Jury{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): @@ -558,14 +600,21 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. def formsemestre_list_archives(formsemestre_id): """Page listing archives""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) sem_archive_id = formsemestre_id L = [] - for archive_id in PVArchive.list_obj_archives(sem_archive_id): + for archive_id in PV_ARCHIVER.list_obj_archives( + sem_archive_id, dept_id=formsemestre.dept_id + ): a = { "archive_id": archive_id, - "description": PVArchive.get_archive_description(archive_id), - "date": PVArchive.get_archive_date(archive_id), - "content": PVArchive.list_archive(archive_id), + "description": PV_ARCHIVER.get_archive_description( + archive_id, dept_id=formsemestre.dept_id + ), + "date": PV_ARCHIVER.get_archive_date(archive_id), + "content": PV_ARCHIVER.list_archive( + archive_id, dept_id=formsemestre.dept_id + ), } L.append(a) @@ -575,7 +624,7 @@ def formsemestre_list_archives(formsemestre_id): else: H.append("
    ") for a in L: - archive_name = PVArchive.get_archive_name(a["archive_id"]) + archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"]) H.append( '
  • %s : %s (supprimer)
      ' % ( @@ -602,7 +651,9 @@ def formsemestre_get_archived_file(formsemestre_id, archive_name, filename): """Send file to client.""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) sem_archive_id = formsemestre.id - return PVArchive.get_archived_file(sem_archive_id, archive_name, filename) + return PV_ARCHIVER.get_archived_file( + sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id + ) def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False): @@ -617,7 +668,9 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= ) ) sem_archive_id = formsemestre_id - archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name) + archive_id = PV_ARCHIVER.get_id_from_name( + sem_archive_id, archive_name, dept_id=formsemestre.dept_id + ) dest_url = url_for( "notes.formsemestre_list_archives", @@ -628,7 +681,7 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= if not dialog_confirmed: return scu.confirm_dialog( f"""

      Confirmer la suppression de l'archive du { - PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") + PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") } ?

      La suppression sera définitive.

      """, @@ -640,6 +693,6 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= }, ) - PVArchive.delete_archive(archive_id) + PV_ARCHIVER.delete_archive(archive_id, dept_id=formsemestre.dept_id) flash("Archive supprimée") return flask.redirect(dest_url) diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index b538d0f5..4bc76a10 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -52,7 +52,8 @@ class EtudsArchiver(sco_archives.BaseArchiver): sco_archives.BaseArchiver.__init__(self, archive_type="docetuds") -EtudsArchive = EtudsArchiver() +# Global au processus, attention ! +ETUDS_ARCHIVER = EtudsArchiver() def can_edit_etud_archive(authuser): @@ -60,21 +61,21 @@ def can_edit_etud_archive(authuser): return authuser.has_permission(Permission.ScoEtudAddAnnotations) -def etud_list_archives_html(etudid): +def etud_list_archives_html(etud: Identite): """HTML snippet listing archives""" can_edit = can_edit_etud_archive(current_user) - etuds = sco_etud.get_etud_info(etudid=etudid) - if not etuds: - raise ScoValueError("étudiant inexistant") - etud = etuds[0] - etud_archive_id = etudid + etud_archive_id = etud.id L = [] - for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): + for archive_id in ETUDS_ARCHIVER.list_obj_archives( + etud_archive_id, dept_id=etud.dept_id + ): a = { "archive_id": archive_id, - "description": EtudsArchive.get_archive_description(archive_id), - "date": EtudsArchive.get_archive_date(archive_id), - "content": EtudsArchive.list_archive(archive_id), + "description": ETUDS_ARCHIVER.get_archive_description( + archive_id, dept_id=etud.dept_id + ), + "date": ETUDS_ARCHIVER.get_archive_date(archive_id), + "content": ETUDS_ARCHIVER.list_archive(archive_id, dept_id=etud.dept_id), } L.append(a) delete_icon = scu.icontag( @@ -85,7 +86,7 @@ def etud_list_archives_html(etudid): ) H = ['
        '] for a in L: - archive_name = EtudsArchive.get_archive_name(a["archive_id"]) + archive_name = ETUDS_ARCHIVER.get_archive_name(a["archive_id"]) H.append( """
      • %s""" % (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"]) @@ -93,14 +94,14 @@ def etud_list_archives_html(etudid): for filename in a["content"]: H.append( """%s""" - % (etudid, archive_name, filename, filename) + % (etud.id, archive_name, filename, filename) ) if not a["content"]: H.append("aucun fichier !") if can_edit: H.append( '%s' - % (etudid, archive_name, delete_icon) + % (etud.id, archive_name, delete_icon) ) else: H.append('' + delete_disabled_icon + "") @@ -108,7 +109,7 @@ def etud_list_archives_html(etudid): if can_edit: H.append( '
      • ajouter un fichier
      • ' - % etudid + % etud.id ) H.append("
      ") return "".join(H) @@ -121,12 +122,13 @@ def add_archives_info_to_etud_list(etuds): for etud in etuds: l = [] etud_archive_id = etud["etudid"] - for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): + # Here, ETUDS_ARCHIVER will use g.dept_id + for archive_id in ETUDS_ARCHIVER.list_obj_archives(etud_archive_id): l.append( "%s (%s)" % ( - EtudsArchive.get_archive_description(archive_id), - EtudsArchive.list_archive(archive_id)[0], + ETUDS_ARCHIVER.get_archive_description(archive_id), + ETUDS_ARCHIVER.list_archive(archive_id)[0], ) ) etud["etudarchive"] = ", ".join(l) @@ -197,8 +199,8 @@ def _store_etud_file_to_new_archive( filesize = len(data) if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE: return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})" - archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description) - EtudsArchive.store(archive_id, filename, data) + archive_id = ETUDS_ARCHIVER.create_obj_archive(etud_archive_id, description) + ETUDS_ARCHIVER.store(archive_id, filename, data) return True, "ok" @@ -212,14 +214,16 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): raise ScoValueError("étudiant inexistant") etud = etuds[0] etud_archive_id = etud["etudid"] - archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name) + archive_id = ETUDS_ARCHIVER.get_id_from_name( + etud_archive_id, archive_name, dept_id=etud["dept_id"] + ) if not dialog_confirmed: return scu.confirm_dialog( """

      Confirmer la suppression des fichiers ?

      Fichier associé le %s à l'étudiant %s

      La suppression sera définitive.

      """ % ( - EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), + ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), etud["nomprenom"], ), dest_url="", @@ -232,7 +236,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): parameters={"etudid": etudid, "archive_name": archive_name}, ) - EtudsArchive.delete_archive(archive_id) + ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"]) flash("Archive supprimée") return flask.redirect( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) @@ -246,7 +250,9 @@ def etud_get_archived_file(etudid, archive_name, filename): raise ScoValueError("étudiant inexistant") etud = etuds[0] etud_archive_id = etud["etudid"] - return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename) + return ETUDS_ARCHIVER.get_archived_file( + etud_archive_id, archive_name, filename, dept_id=etud["dept_id"] + ) # --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 0030d203..c6dbd447 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -12,11 +12,14 @@ from app.scodoc.sco_archives import BaseArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_utils import is_iso_formated +from app import log + class Trace: """gestionnaire de la trace des fichiers justificatifs""" def __init__(self, path: str) -> None: + log(f"init Trace {path}") self.path: str = path + "/_trace.csv" self.content: dict[str, list[datetime, datetime, str]] = {} self.import_from_file() @@ -45,7 +48,7 @@ class Trace: if fname in modes: continue traced: list[datetime, datetime, str] = self.content.get(fname, False) - if not traced: + if not traced or mode == "entry": self.content[fname] = [None, None, None] traced = self.content[fname] @@ -102,7 +105,7 @@ class JustificatifArchiver(BaseArchiver): def save_justificatif( self, - etudid: int, + etud: Identite, filename: str, data: bytes or str, archive_name: str = None, @@ -113,17 +116,18 @@ class JustificatifArchiver(BaseArchiver): Ajoute un fichier dans une archive "justificatif" pour l'etudid donné Retourne l'archive_name utilisé """ - self._set_dept(etudid) if archive_name is None: archive_id: str = self.create_obj_archive( - oid=etudid, description=description + oid=etud.id, description=description, dept_id=etud.dept_id ) else: - archive_id: str = self.get_id_from_name(etudid, archive_name) + archive_id: str = self.get_id_from_name( + etud.id, archive_name, dept_id=etud.dept_id + ) - fname: str = self.store(archive_id, filename, data) - - trace = Trace(self.get_obj_dir(etudid)) + fname: str = self.store(archive_id, filename, data, dept_id=etud.dept_id) + log(f"obj_dir {self.get_obj_dir(etud.id, dept_id=etud.dept_id)} | {archive_id}") + trace = Trace(archive_id) trace.set_trace(fname, mode="entry") if user_id is not None: trace.set_trace(fname, mode="user_id", current_user=user_id) @@ -132,7 +136,7 @@ class JustificatifArchiver(BaseArchiver): def delete_justificatif( self, - etudid: int, + etud: Identite, archive_name: str, filename: str = None, has_trace: bool = True, @@ -140,92 +144,84 @@ class JustificatifArchiver(BaseArchiver): """ Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné - Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant + Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) + dans la trace de l'étudiant """ - self._set_dept(etudid) - if str(etudid) not in self.list_oids(): - raise ValueError(f"Aucune archive pour etudid[{etudid}]") + if str(etud.id) not in self.list_oids(etud.dept_id): + raise ValueError(f"Aucune archive pour etudid[{etud.id}]") - archive_id = self.get_id_from_name(etudid, archive_name) + archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) if filename is not None: - if filename not in self.list_archive(archive_id): + if filename not in self.list_archive(archive_id, dept_id=etud.dept_id): raise ValueError( - f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]" + f"""filename {filename} inconnu dans l'archive archive_id[{ + archive_id}] -> etudid[{etud.id}]""" ) - path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename) + path: str = os.path.join( + self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, filename + ) if os.path.isfile(path): if has_trace: - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(archive_id) trace.set_trace(filename, mode="delete") os.remove(path) else: if has_trace: - trace = Trace(self.get_obj_dir(etudid)) - trace.set_trace(*self.list_archive(archive_id), mode="delete") + trace = Trace(archive_id) + trace.set_trace( + *self.list_archive(archive_id, dept_id=etud.dept_id), mode="delete" + ) self.delete_archive( os.path.join( - self.get_obj_dir(etudid), + self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, ) ) def list_justificatifs( - self, archive_name: str, etudid: int + self, archive_name: str, etud: Identite ) -> list[tuple[str, int]]: """ Retourne la liste des noms de fichiers dans l'archive donnée """ - self._set_dept(etudid) filenames: list[str] = [] - archive_id = self.get_id_from_name(etudid, archive_name) + archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) - filenames = self.list_archive(archive_id) - trace: Trace = Trace(self.get_obj_dir(etudid)) + filenames = self.list_archive(archive_id, dept_id=etud.dept_id) + trace: Trace = Trace(archive_id) traced = trace.get_trace(filenames) retour = [(key, value[2]) for key, value in traced.items()] return retour - def get_justificatif_file(self, archive_name: str, etudid: int, filename: str): + def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str): """ Retourne une réponse de téléchargement de fichier si le fichier existe """ - self._set_dept(etudid) - archive_id: str = self.get_id_from_name(etudid, archive_name) - if filename in self.list_archive(archive_id): - return self.get_archived_file(etudid, archive_name, filename) + archive_id: str = self.get_id_from_name( + etud.id, archive_name, dept_id=etud.dept_id + ) + if filename in self.list_archive(archive_id, dept_id=etud.dept_id): + return self.get_archived_file( + etud.id, archive_name, filename, dept_id=etud.dept_id + ) raise ScoValueError( f"Fichier {filename} introuvable dans l'archive {archive_name}" ) - def _set_dept(self, etudid: int): - """ - Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant - """ - etud: Identite = Identite.query.filter_by(id=etudid).first() - self.set_dept_id(etud.dept_id) - def remove_dept_archive(self, dept_id: int = None): """ Supprime toutes les archives d'un département (ou de tous les départements) ⚠ Supprime aussi les fichiers de trace ⚠ """ - self.set_dept_id(1) - self.initialize() - + # juste pour récupérer .root, dept_id n'a pas d'importance + self.initialize(dept_id=1) if dept_id is None: rmtree(self.root, ignore_errors=True) else: rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True) - - def get_trace( - self, etudid: int, *fnames: str - ) -> dict[str, list[datetime, datetime]]: - """Récupère la trace des justificatifs de l'étudiant""" - trace = Trace(self.get_obj_dir(etudid)) - return trace.get_trace(fnames) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 2a478843..c80a5f55 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -4,9 +4,9 @@ Ecrit par Matthias Hartmann. from datetime import date, datetime, time, timedelta from pytz import UTC -from app import log +from app import log, db import app.scodoc.sco_utils as scu -from app.models.assiduites import Assiduite, Justificatif +from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.scodoc import sco_formsemestre_inscriptions @@ -141,12 +141,9 @@ class CountCalculator: self.hours += finish_hours.total_seconds() / 3600 self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) - def compute_assiduites(self, assiduites: Assiduite): + def compute_assiduites(self, assiduites: Query | list): """Calcule les métriques pour la collection d'assiduité donnée""" assi: Assiduite - assiduites: list[Assiduite] = ( - assiduites.all() if isinstance(assiduites, Assiduite) else assiduites - ) for assi in assiduites: self.count += 1 delta: timedelta = assi.date_fin - assi.date_debut @@ -167,7 +164,7 @@ class CountCalculator: self.hours += delta.total_seconds() / 3600 - def to_dict(self) -> dict[str, int or float]: + def to_dict(self) -> dict[str, int | float]: """Retourne les métriques sous la forme d'un dictionnaire""" return { "compte": self.count, @@ -179,7 +176,7 @@ class CountCalculator: def get_assiduites_stats( assiduites: Query, metric: str = "all", filtered: dict[str, object] = None -) -> dict[str, int or float]: +) -> dict[str, int | float]: """Compte les assiduités en fonction des filtres""" if filtered is not None: @@ -212,7 +209,7 @@ def get_assiduites_stats( output: dict = {} calculator: CountCalculator = CountCalculator() - if "split" not in filtered: + if filtered is None or "split" not in filtered: calculator.compute_assiduites(assiduites) count: dict = calculator.to_dict() @@ -276,7 +273,7 @@ def filter_assiduites_by_est_just(assiduites: Assiduite, est_just: bool) -> Quer def filter_by_user_id( - collection: Assiduite or Justificatif, + collection: Assiduite | Justificatif, user_id: int, ) -> Query: """ @@ -286,8 +283,8 @@ def filter_by_user_id( def filter_by_date( - collection: Assiduite or Justificatif, - collection_cls: Assiduite or Justificatif, + collection: Assiduite | Justificatif, + collection_cls: Assiduite | Justificatif, date_deb: datetime = None, date_fin: datetime = None, strict: bool = False, @@ -311,7 +308,7 @@ def filter_by_date( ) -def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Query: +def filter_justificatifs_by_etat(justificatifs: Query, etat: str) -> Query: """ Filtrage d'une collection de justificatifs en fonction de leur état """ @@ -320,7 +317,7 @@ def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Quer return justificatifs.filter(Justificatif.etat.in_(etats)) -def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) -> Query: +def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int | None) -> Query: """ Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl """ @@ -328,8 +325,8 @@ def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) -> def filter_by_formsemestre( - collection_query: Assiduite or Justificatif, - collection_class: Assiduite or Justificatif, + collection_query: Assiduite | Justificatif, + collection_class: Assiduite | Justificatif, formsemestre: FormSemestre, ) -> Query: """ @@ -358,7 +355,7 @@ def filter_by_formsemestre( return collection_result.filter(collection_class.date_fin <= form_date_fin) -def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: +def justifies(justi: Justificatif, obj: bool = False) -> list[int] | Query: """ Retourne la liste des assiduite_id qui sont justifié par la justification Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT @@ -382,7 +379,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: def get_all_justified( - etudid: int, date_deb: datetime = None, date_fin: datetime = None + etudid: int, + date_deb: datetime = None, + date_fin: datetime = None, + moduleimpl_id: int = None, ) -> Query: """Retourne toutes les assiduités justifiées sur une période""" @@ -393,7 +393,9 @@ def get_all_justified( date_deb = scu.localize_datetime(date_deb) date_fin = scu.localize_datetime(date_fin) - justified = Assiduite.query.filter_by(est_just=True, etudid=etudid) + justified: Query = Assiduite.query.filter_by(est_just=True, etudid=etudid) + if moduleimpl_id is not None: + justified = justified.filter_by(moduleimpl_id=moduleimpl_id) after = filter_by_date( justified, Assiduite, @@ -403,6 +405,42 @@ def get_all_justified( return after +def create_absence( + date_debut: datetime, + date_fin: datetime, + etudid: int, + description: str = None, + est_just: bool = False, +) -> int: + etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404() + assiduite_unique: Assiduite = Assiduite.create_assiduite( + etud=etud, + date_debut=date_debut, + date_fin=date_fin, + etat=scu.EtatAssiduite.ABSENT, + description=description, + ) + db.session.add(assiduite_unique) + + db.session.commit() + if est_just: + justi = Justificatif.create_justificatif( + etud=etud, + date_debut=date_debut, + date_fin=date_fin, + etat=scu.EtatJustificatif.VALIDE, + raison=description, + ) + db.session.add(justi) + db.session.commit() + + compute_assiduites_justified(etud.id, [justi]) + + calculator: CountCalculator = CountCalculator() + calculator.compute_assiduites([assiduite_unique]) + return calculator.to_dict()["demi"] + + # Gestion du cache def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: @@ -419,7 +457,7 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: def formsemestre_get_assiduites_count( - etudid: int, formsemestre: FormSemestre + 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) @@ -428,9 +466,14 @@ def formsemestre_get_assiduites_count( metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id) return get_assiduites_count_in_interval( etudid, - date_debut=formsemestre.date_debut, - date_fin=formsemestre.date_fin, + date_debut=scu.localize_datetime( + datetime.combine(formsemestre.date_debut, time(8, 0)) + ), + date_fin=scu.localize_datetime( + datetime.combine(formsemestre.date_fin, time(18, 0)) + ), metrique=scu.translate_assiduites_metric(metrique), + moduleimpl_id=moduleimpl_id, ) @@ -441,6 +484,7 @@ def get_assiduites_count_in_interval( metrique="demi", date_debut: datetime = None, date_fin: datetime = None, + moduleimpl_id: int = None, ): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: tuple (nb abs, nb abs justifiées) @@ -452,33 +496,39 @@ def get_assiduites_count_in_interval( key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites" r = sco_cache.AbsSemEtudCache.get(key) - if not r: + if not r or moduleimpl_id is not None: date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso) date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso) - assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) + assiduites: Query = Assiduite.query.filter_by(etudid=etudid) assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin) + + if moduleimpl_id is not None: + assiduites = assiduites.filter_by(moduleimpl_id=moduleimpl_id) + justificatifs = filter_by_date( justificatifs, Justificatif, date_debut, date_fin ) - calculator: CountCalculator = CountCalculator() calculator.compute_assiduites(assiduites) nb_abs: dict = calculator.to_dict()[metrique] - abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin) + abs_just: list[Assiduite] = get_all_justified( + etudid, date_debut, date_fin, moduleimpl_id + ) calculator.reset() calculator.compute_assiduites(abs_just) nb_abs_just: dict = calculator.to_dict()[metrique] r = (nb_abs, nb_abs_just) - ans = sco_cache.AbsSemEtudCache.set(key, r) - if not ans: - log("warning: get_assiduites_count failed to cache") + if moduleimpl_id is None: + ans = sco_cache.AbsSemEtudCache.set(key, r) + if not ans: + log("warning: get_assiduites_count failed to cache") return r @@ -544,7 +594,7 @@ def invalidate_assiduites_etud_date(etudid, date: datetime): invalidate_assiduites_count(etudid, sem) -def simple_invalidate_cache(obj: dict, etudid: str or int = None): +def simple_invalidate_cache(obj: dict, etudid: str | int = None): """Invalide le cache de l'étudiant et du / des semestres""" date_debut = ( obj["date_debut"] diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index a1e67d33..b1600c1d 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -95,7 +95,7 @@ def get_formsemestre_bulletin_etud_json( return formsemestre_bulletinetud( etud, formsemestre_id=formsemestre.id, - format="json", + fmt="json", version=version, xml_with_decisions=True, force_publishing=force_publishing, @@ -201,7 +201,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): infos, dpv = etud_descr_situation_semestre( etudid, formsemestre, - format="html", + fmt="html", show_date_inscr=prefs["bul_show_date_inscr"], show_decisions=prefs["bul_show_decision"], show_uevalid=prefs["bul_show_uevalid"], @@ -582,7 +582,7 @@ def _ue_mod_bulletin( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=e.id, - format="html", + fmt="html", tf_submitted=1, ) e_dict[ @@ -679,14 +679,14 @@ def etud_descr_situation_semestre( etudid, formsemestre: FormSemestre, ne="", - format="html", # currently unused + fmt="html", # currently unused show_decisions=True, show_uevalid=True, show_date_inscr=True, show_mention=False, ): """Dict décrivant la situation de l'étudiant dans ce semestre. - Si format == 'html', peut inclure du balisage html (actuellement inutilisé) + Si fmt == 'html', peut inclure du balisage html (actuellement inutilisé) situation : chaine résumant en français la situation de l'étudiant. Par ex. "Inscrit le 31/12/1999. Décision jury: Validé. ..." @@ -889,7 +889,7 @@ def _format_situation_fields( def formsemestre_bulletinetud( etud: Identite = None, formsemestre_id=None, - format=None, + fmt=None, version="long", xml_with_decisions=False, force_publishing=False, # force publication meme si semestre non publie sur "portail" @@ -910,7 +910,7 @@ def formsemestre_bulletinetud( - prefer_mail_perso: pour pdfmail, utilise adresse mail perso en priorité. """ - format = format or "html" + fmt = fmt or "html" formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) if not formsemestre: raise ScoValueError(f"semestre {formsemestre_id} inconnu !") @@ -918,21 +918,21 @@ def formsemestre_bulletinetud( bulletin = do_formsemestre_bulletinetud( formsemestre, etud, - format=format, + fmt=fmt, version=version, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, prefer_mail_perso=prefer_mail_perso, )[0] - if format not in {"html", "pdfmail"}: + if fmt not in {"html", "pdfmail"}: filename = scu.bul_filename(formsemestre, etud) - mime, suffix = scu.get_mime_suffix(format) + mime, suffix = scu.get_mime_suffix(fmt) return scu.send_file(bulletin, filename, mime=mime, suffix=suffix) - elif format == "pdfmail": + elif fmt == "pdfmail": return "" H = [ - _formsemestre_bulletinetud_header_html(etud, formsemestre, format, version), + _formsemestre_bulletinetud_header_html(etud, formsemestre, fmt, version), bulletin, render_template( "bul_foot.j2", @@ -963,7 +963,7 @@ def do_formsemestre_bulletinetud( formsemestre: FormSemestre, etud: Identite, version="long", # short, long, selectedevals - format=None, + fmt=None, xml_with_decisions: bool = False, force_publishing: bool = False, prefer_mail_perso: bool = False, @@ -985,8 +985,8 @@ def do_formsemestre_bulletinetud( où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) et filigranne est un message à placer en "filigranne" (eg "Provisoire"). """ - format = format or "html" - if format == "xml": + fmt = fmt or "html" + if fmt == "xml": bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud( formsemestre.id, etud.id, @@ -997,7 +997,7 @@ def do_formsemestre_bulletinetud( return bul, "" - elif format == "json": # utilisé pour classic et "oldjson" + elif fmt == "json": # utilisé pour classic et "oldjson" bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( formsemestre.id, etud.id, @@ -1015,23 +1015,23 @@ def do_formsemestre_bulletinetud( else: bul_dict = formsemestre_bulletinetud_dict(formsemestre.id, etud.id) - if format == "html": + if fmt == "html": htm, _ = sco_bulletins_generator.make_formsemestre_bulletin_etud( bul_dict, etud=etud, formsemestre=formsemestre, version=version, fmt="html" ) return htm, bul_dict["filigranne"] - elif format == "pdf" or format == "pdfpart": + if fmt == "pdf" or fmt == "pdfpart": bul, filename = sco_bulletins_generator.make_formsemestre_bulletin_etud( bul_dict, etud=etud, formsemestre=formsemestre, version=version, fmt="pdf", - stand_alone=(format != "pdfpart"), + stand_alone=(fmt != "pdfpart"), with_img_signatures_pdf=with_img_signatures_pdf, ) - if format == "pdf": + if fmt == "pdf": return ( scu.sendPDFFile(bul, filename), bul_dict["filigranne"], @@ -1039,7 +1039,7 @@ def do_formsemestre_bulletinetud( else: return bul, bul_dict["filigranne"] - elif format == "pdfmail": + elif fmt == "pdfmail": # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html # check permission if not can_send_bulletin_by_mail(formsemestre.id): @@ -1067,7 +1067,7 @@ def do_formsemestre_bulletinetud( return True, bul_dict["filigranne"] - raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({format})") + raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({fmt})") def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): @@ -1097,10 +1097,12 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): hea = "" if sco_preferences.get_preference("bul_mail_list_abs"): - hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE - # sco_abs_views.ListeAbsEtud( - # etud["etudid"], with_evals=False, format="text" - # ) + from app.views.assiduites import generate_bul_list + + etud_identite: Identite = Identite.get_etud(etud["etudid"]) + form_semestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) + hea += "\n\n" + hea += generate_bul_list(etud_identite, form_semestre) subject = f"""Relevé de notes de {etud["nomprenom"]}""" recipients = [recipient_addr] @@ -1154,7 +1156,7 @@ def make_menu_autres_operations( "formsemestre_id": formsemestre.id, "etudid": etud.id, "version": version, - "format": "pdf", + "fmt": "pdf", }, }, { @@ -1164,7 +1166,7 @@ def make_menu_autres_operations( "formsemestre_id": formsemestre.id, "etudid": etud.id, "version": version, - "format": "pdfmail", + "fmt": "pdfmail", }, # possible slt si on a un mail... "enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id), @@ -1176,7 +1178,7 @@ def make_menu_autres_operations( "formsemestre_id": formsemestre.id, "etudid": etud.id, "version": version, - "format": "pdfmail", + "fmt": "pdfmail", "prefer_mail_perso": 1, }, # possible slt si on a un mail... @@ -1189,7 +1191,7 @@ def make_menu_autres_operations( "formsemestre_id": formsemestre.id, "etudid": etud.id, "version": version, - "format": "json", + "fmt": "json", }, }, { @@ -1199,7 +1201,7 @@ def make_menu_autres_operations( "formsemestre_id": formsemestre.id, "etudid": etud.id, "version": version, - "format": "xml", + "fmt": "xml", }, }, { @@ -1267,7 +1269,7 @@ def make_menu_autres_operations( def _formsemestre_bulletinetud_header_html( etud, formsemestre: FormSemestre, - format=None, + fmt=None, version=None, ): H = [ @@ -1283,7 +1285,7 @@ def _formsemestre_bulletinetud_header_html( render_template( "bul_head.j2", etud=etud, - format=format, + fmt=fmt, formsemestre=formsemestre, menu_autres_operations=make_menu_autres_operations( etud=etud, diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index bf6660d8..2ec0e239 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -35,7 +35,7 @@ class BulletinGenerator: .bul_part_below(fmt) .bul_signatures_pdf() - .__init__ et .generate(format) methodes appelees par le client (sco_bulletin) + .__init__ et .generate(fmt) methodes appelees par le client (sco_bulletin) La préférence 'bul_class_name' donne le nom de la classe generateur. La préférence 'bul_pdf_class_name' est obsolete (inutilisée). @@ -62,7 +62,7 @@ from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from flask import request from flask_login import current_user -from app.models import FormSemestre, Identite +from app.models import FormSemestre, Identite, ScoDocSiteConfig from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import NoteProcessError from app import log @@ -139,18 +139,18 @@ class BulletinGenerator: sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"]) return scu.bul_filename_old(sem, self.bul_dict["etud"], "pdf") - def generate(self, format="", stand_alone=True): + def generate(self, fmt="", stand_alone=True): """Return bulletin in specified format""" - if not format in self.supported_formats: - raise ValueError(f"unsupported bulletin format ({format})") + if not fmt in self.supported_formats: + raise ValueError(f"unsupported bulletin format ({fmt})") try: PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant - if format == "html": + if fmt == "html": return self.generate_html() - elif format == "pdf": + elif fmt == "pdf": return self.generate_pdf(stand_alone=stand_alone) else: - raise ValueError(f"invalid bulletin format ({format})") + raise ValueError(f"invalid bulletin format ({fmt})") finally: PDFLOCK.release() @@ -197,6 +197,10 @@ class BulletinGenerator: else: # Insere notre marqueur qui permet de générer les bookmarks et filigrannes: story.insert(index_obj_debut, marque_debut_bulletin) + + if ScoDocSiteConfig.is_bul_pdf_disabled(): + story = [Paragraph("

      Export des PDF interdit par l'administrateur

      ")] + # # objects.append(sco_pdf.FinBulletin()) if not stand_alone: @@ -288,8 +292,10 @@ def make_formsemestre_bulletin_etud( ): if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"): gen_class = bulletin_get_class(bul_class_name + "BUT") - if gen_class is None: + if gen_class is None and bul_dict.get("type") != "BUT": gen_class = bulletin_get_class(bul_class_name) + if gen_class is not None: + break if gen_class is None: raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})") @@ -324,7 +330,7 @@ def make_formsemestre_bulletin_etud( version=version, with_img_signatures_pdf=with_img_signatures_pdf, ) - data = bul_generator.generate(format=fmt, stand_alone=stand_alone) + data = bul_generator.generate(fmt=fmt, stand_alone=stand_alone) finally: PDFLOCK.release() diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index e5e7693d..df547769 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -405,6 +405,7 @@ def dict_decision_jury( """dict avec decision pour bulletins json - autorisation_inscription - decision : décision semestre + - decision_annee : annee BUT - decision_ue : list des décisions UE - situation diff --git a/app/scodoc/sco_bulletins_legacy.py b/app/scodoc/sco_bulletins_legacy.py index 5ba649f9..9650e74a 100644 --- a/app/scodoc/sco_bulletins_legacy.py +++ b/app/scodoc/sco_bulletins_legacy.py @@ -252,7 +252,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator): elif fmt == "html": return self.bul_part_below_html() else: - raise ValueError("invalid bulletin format (%s)" % fmt) + raise ValueError(f"invalid bulletin format ({fmt})") def bul_part_below_pdf(self): """ diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 01c39aef..a3786c82 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -146,15 +146,15 @@ def process_field( field, cdict, style, suppress_empty_pars=False, fmt="pdf", field_name=None ): """Process a field given in preferences, returns - - if format = 'pdf': a list of Platypus objects - - if format = 'html' : a string + - if fmt = 'pdf': a list of Platypus objects + - if fmt = 'html' : a string Substitutes all %()s markup Remove potentialy harmful tags Replaces by - If format = 'html', replaces by

      . HTML does not allow logos. + If fmt = 'html', replaces by

      . HTML does not allow logos. """ try: # None values are mapped to empty strings by WrapDict @@ -225,7 +225,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"): frag, _ = sco_bulletins.do_formsemestre_bulletinetud( formsemestre, etud, - format="pdfpart", + fmt="pdfpart", version=version, ) fragments += frag @@ -270,7 +270,7 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( formsemestre, etud, - format="pdfpart", + fmt="pdfpart", version=version, ) fragments += frag diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index e8dda75f..ab49f8a8 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -116,7 +116,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): html_with_td_classes=True, ) - return T.gen(format=fmt) + return T.gen(fmt=fmt) def bul_part_below(self, fmt="html"): """Génère les informations placées sous la table de notes diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index 3eafc5ca..58305f14 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -357,7 +357,7 @@ def make_xml_formsemestre_bulletinetud( infos, dpv = sco_bulletins.etud_descr_situation_semestre( etudid, formsemestre, - format="xml", + fmt="xml", show_uevalid=sco_preferences.get_preference( "bul_show_uevalid", formsemestre_id ), diff --git a/app/scodoc/sco_cost_formation.py b/app/scodoc/sco_cost_formation.py index 87937b5e..f266b063 100644 --- a/app/scodoc/sco_cost_formation.py +++ b/app/scodoc/sco_cost_formation.py @@ -152,7 +152,7 @@ def formsemestre_estim_cost( n_group_tp=1, coef_tp=1, coef_cours=1.5, - format="html", + fmt="html", ): """Page (formulaire) estimation coûts""" @@ -192,4 +192,4 @@ def formsemestre_estim_cost( coef_tp, ) - return tab.make_page(format=format) + return tab.make_page(fmt=fmt) diff --git a/app/scodoc/sco_debouche.py b/app/scodoc/sco_debouche.py index 09ae1f5a..c08a6e99 100644 --- a/app/scodoc/sco_debouche.py +++ b/app/scodoc/sco_debouche.py @@ -49,7 +49,7 @@ from app.scodoc import sco_etud import sco_version -def report_debouche_date(start_year=None, format="html"): +def report_debouche_date(start_year=None, fmt="html"): """Rapport (table) pour les débouchés des étudiants sortis à partir de l'année indiquée. """ @@ -63,7 +63,7 @@ def report_debouche_date(start_year=None, format="html"): "Année invalide. Année de début de la recherche" ) - if format == "xls": + if fmt == "xls": keep_numeric = True # pas de conversion des notes en strings else: keep_numeric = False @@ -81,7 +81,7 @@ def report_debouche_date(start_year=None, format="html"): title="""

      Débouchés étudiants

      """, init_qtip=True, javascripts=["js/etud_info.js"], - format=format, + fmt=fmt, with_html_headers=True, ) @@ -276,7 +276,7 @@ def itemsuivi_suppress(itemsuivi_id): return ("", 204) -def itemsuivi_create(etudid, item_date=None, situation="", format=None): +def itemsuivi_create(etudid, item_date=None, situation="", fmt=None): """Creation d'un item""" if not sco_permissions_check.can_edit_suivi(): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") @@ -287,7 +287,7 @@ def itemsuivi_create(etudid, item_date=None, situation="", format=None): logdb(cnx, method="itemsuivi_create", etudid=etudid) log("created itemsuivi %s for %s" % (itemsuivi_id, etudid)) item = itemsuivi_get(cnx, itemsuivi_id) - if format == "json": + if fmt == "json": return scu.sendJSON(item) return item @@ -320,13 +320,13 @@ def itemsuivi_set_situation(object, value): return situation or scu.IT_SITUATION_MISSING_STR -def itemsuivi_list_etud(etudid, format=None): +def itemsuivi_list_etud(etudid, fmt=None): """Liste des items pour cet étudiant, avec tags""" cnx = ndb.GetDBConnexion() items = _itemsuivi_list(cnx, {"etudid": etudid}) for it in items: it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"])) - if format == "json": + if fmt == "json": return scu.sendJSON(items) return items diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 39360d5f..80047fcd 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -979,18 +979,18 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
    • Export XML de la formation ou sans codes Apogée (permet de l'enregistrer pour l'échanger avec un autre site)
    • Export JSON de la formation
    • diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py index 01081021..ce0f5457 100644 --- a/app/scodoc/sco_etape_apogee.py +++ b/app/scodoc/sco_etape_apogee.py @@ -85,7 +85,7 @@ class ApoCSVArchiver(sco_archives.BaseArchiver): sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv") -ApoCSVArchive = ApoCSVArchiver() +APO_CSV_ARCHIVER = ApoCSVArchiver() # def get_sem_apo_archive(formsemestre_id): @@ -126,9 +126,9 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id): oid = f"{annee_scolaire}-{sem_id}" description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}""" - archive_id = ApoCSVArchive.create_obj_archive(oid, description) + archive_id = APO_CSV_ARCHIVER.create_obj_archive(oid, description) csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING) - ApoCSVArchive.store(archive_id, filename, csv_data_bytes) + APO_CSV_ARCHIVER.store(archive_id, filename, csv_data_bytes) return apo_data.etape @@ -138,7 +138,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): :return: list of informations about stored CSV [ { } ] """ - oids = ApoCSVArchive.list_oids() # [ '2016-1', ... ] + oids = APO_CSV_ARCHIVER.list_oids() # [ '2016-1', ... ] # filter if annee_scolaire: e = re.compile(str(annee_scolaire) + "-.+") @@ -149,9 +149,9 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): infos = [] # liste d'infos for oid in oids: - archive_ids = ApoCSVArchive.list_obj_archives(oid) + archive_ids = APO_CSV_ARCHIVER.list_obj_archives(oid) for archive_id in archive_ids: - description = ApoCSVArchive.get_archive_description(archive_id) + description = APO_CSV_ARCHIVER.get_archive_description(archive_id) fs = tuple(description.split(";")) if len(fs) == 3: arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs @@ -165,7 +165,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): "annee_scolaire": int(arch_annee_scolaire), "sem_id": int(arch_sem_id), "etape_apo": arch_etape_apo, # qui contient éventuellement le VDI - "date": ApoCSVArchive.get_archive_date(archive_id), + "date": APO_CSV_ARCHIVER.get_archive_date(archive_id), } ) infos.sort(key=lambda x: x["etape_apo"]) @@ -185,7 +185,7 @@ def apo_csv_list_stored_etapes(annee_scolaire, sem_id=None, etapes=None): def apo_csv_delete(archive_id): """Delete archived CSV""" - ApoCSVArchive.delete_archive(archive_id) + APO_CSV_ARCHIVER.delete_archive(archive_id) def apo_csv_get_archive(etape_apo, annee_scolaire="", sem_id=""): @@ -209,7 +209,7 @@ def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str: "Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id) ) archive_id = info["archive_id"] - data = ApoCSVArchive.get(archive_id, etape_apo + ".csv") + data = APO_CSV_ARCHIVER.get(archive_id, etape_apo + ".csv") # ce fichier a été archivé donc généré par ScoDoc # son encodage est donc APO_OUTPUT_ENCODING return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING) diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py index c5ae128e..8f11532b 100644 --- a/app/scodoc/sco_etape_apogee_view.py +++ b/app/scodoc/sco_etape_apogee_view.py @@ -495,7 +495,7 @@ def table_apo_csv_list(semset): return tab -def view_apo_etuds(semset_id, title="", nip_list="", format="html"): +def view_apo_etuds(semset_id, title="", nip_list="", fmt="html"): """Table des étudiants Apogée par nips nip_list est une chaine, codes nip séparés par des , """ @@ -530,11 +530,11 @@ def view_apo_etuds(semset_id, title="", nip_list="", format="html"): title=title, etuds=list(etuds.values()), keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"), - format=format, + fmt=fmt, ) -def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"): +def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"): """Table des étudiants ScoDoc par nips ou etudids""" if not isinstance(nip_list, str): nip_list = str(nip_list) @@ -553,12 +553,12 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"): title=title, etuds=etuds, keys=("code_nip", "nom", "prenom"), - format=format, + fmt=fmt, ) def _view_etuds_page( - semset_id: int, title="", etuds: list = None, keys=(), format="html" + semset_id: int, title="", etuds: list = None, keys=(), fmt="html" ) -> str: "Affiche les étudiants indiqués" # Tri les étudiants par nom: @@ -581,8 +581,8 @@ def _view_etuds_page( filename="students_apo", preferences=sco_preferences.SemPreferences(), ) - if format != "html": - return tab.make_page(format=format) + if fmt != "html": + return tab.make_page(fmt=fmt) return f""" {html_sco_header.sco_header( @@ -711,9 +711,9 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False): return flask.redirect(dest_url) -def view_apo_csv(etape_apo="", semset_id="", format="html"): +def view_apo_csv(etape_apo="", semset_id="", fmt="html"): """Visualise une maquette stockée - Si format="raw", renvoie le fichier maquette tel quel + Si fmt="raw", renvoie le fichier maquette tel quel """ if not semset_id: raise ValueError("invalid null semset_id") @@ -721,7 +721,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"): annee_scolaire = semset["annee_scolaire"] sem_id = semset["sem_id"] csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id) - if format == "raw": + if fmt == "raw": return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE) apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"]) @@ -798,8 +798,8 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"): preferences=sco_preferences.SemPreferences(), ) - if format != "html": - return tab.make_page(format=format) + if fmt != "html": + return tab.make_page(fmt=fmt) H += [ f""" @@ -807,7 +807,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):

      fichier maquette CSV brut (non rempli par ScoDoc)

      diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py index ec1a8c8b..07c57b05 100644 --- a/app/scodoc/sco_etape_bilan.py +++ b/app/scodoc/sco_etape_bilan.py @@ -668,7 +668,7 @@ class EtapeBilan: self.titres, html_class="repartition", html_with_td_classes=True, - ).gen(format="html") + ).gen(fmt="html") ) return "\n".join(H) @@ -766,7 +766,7 @@ class EtapeBilan: table_id="detail", html_class="table_leftalign", html_sortable=True, - ).gen(format="html") + ).gen(fmt="html") ) return "\n".join(H) diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index d2dea809..41299056 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -30,16 +30,17 @@ from flask import url_for, g from app import db -from app.models import Evaluation, FormSemestre, Identite +from app.models import Evaluation, FormSemestre, Identite, Assiduite import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db -from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_groups +from flask_sqlalchemy.query import Query +from sqlalchemy import or_, and_ + -# XXX TODO-ASSIDUITE https://scodoc.org/git/ScoDoc/ScoDoc/issues/685 def evaluation_check_absences(evaluation: Evaluation): """Vérifie les absences au moment de cette évaluation. Cas incohérents que l'on peut rencontrer pour chaque étudiant: @@ -50,28 +51,30 @@ def evaluation_check_absences(evaluation: Evaluation): EXC et pas justifie Ramene 5 listes d'etudid """ - raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE - - if not evaluation.date_debut: + if not evaluation.date_debut or not evaluation.date_fin: return [], [], [], [], [] # evaluation sans date - am, pm = evaluation.is_matin(), evaluation.is_apresmidi() + etudids = [ + etudid + for etudid, _ in sco_groups.do_evaluation_listeetuds_groups( + evaluation.id, getallstudents=True + ) + ] - # Liste les absences à ce moment: - absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm) - abs_etudids = set([x["etudid"] for x in absences]) # ensemble des etudiants absents - abs_non_just = sco_abs.list_abs_non_just_jour( - evaluation.date_debut.date(), am=am, pm=pm + deb, fin = scu.localize_datetime(evaluation.date_debut), scu.localize_datetime( + evaluation.date_fin ) - abs_nj_etudids = set( - [x["etudid"] for x in abs_non_just] - ) # ensemble des etudiants absents non justifies - justifs = sco_abs.list_abs_jour( - evaluation.date_debut.date(), am=am, pm=pm, is_abs=None, is_just=True + + assiduites: Query = Assiduite.query.filter( + Assiduite.etudid.in_(etudids), + Assiduite.etat == scu.EtatAssiduite.ABSENT, + fin >= Assiduite.date_debut, + deb <= Assiduite.date_fin, ) - just_etudids = set( - [x["etudid"] for x in justifs] - ) # ensemble des etudiants avec justif + + abs_etudids = set(assi.etudid for assi in assiduites) + abs_nj_etudids = set(assi.etudid for assi in assiduites if assi.est_just is False) + just_etudids = set(assi.etudid for assi in assiduites if assi.est_just is True) # Les notes: notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) @@ -80,9 +83,7 @@ def evaluation_check_absences(evaluation: Evaluation): ExcNonSignalee = [] # note EXC mais pas noté absent ExcNonJust = [] # note EXC mais absent non justifie AbsButExc = [] # note ABS mais justifié - for etudid, _ in sco_groups.do_evaluation_listeetuds_groups( - evaluation.id, getallstudents=True - ): + for etudid in etudids: if etudid in notes_db: val = notes_db[etudid]["value"] if ( @@ -108,9 +109,10 @@ def evaluation_check_absences(evaluation: Evaluation): return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc -def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True): +def evaluation_check_absences_html( + evaluation: Evaluation, with_header=True, show_ok=True +): """Affiche état vérification absences d'une évaluation""" - evaluation: Evaluation = db.session.get(Evaluation, evaluation_id) am, pm = evaluation.is_matin(), evaluation.is_apresmidi() # 1 si matin, 0 si apres midi, 2 si toute la journee: match am, pm: @@ -169,14 +171,10 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True ) if linkabs: url = url_for( - "absences.doSignaleAbsence", # XXX TODO-ASSIDUITE - scodoc_dept=g.scodoc_dept, + "assiduites.signal_evaluation_abs", etudid=etudid, - # par defaut signale le jour du début de l'éval - datedebut=evaluation.date_debut.strftime("%d/%m/%Y"), - datefin=evaluation.date_debut.strftime("%d/%m/%Y"), - demijournee=demijournee, - moduleimpl_id=evaluation.moduleimpl_id, + evaluation_id=evaluation.id, + scodoc_dept=g.scodoc_dept, ) H.append( f"""signaler cette absence""" @@ -249,7 +247,7 @@ def formsemestre_check_absences_html(formsemestre_id): ): H.append( evaluation_check_absences_html( - evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ... + evaluation, with_header=False, show_ok=False, ) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index 0bd8333b..125d4880 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -46,7 +46,6 @@ from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc import html_sco_header from app.scodoc import sco_evaluations -from app.scodoc import sco_evaluation_db from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 433f0d44..7c5ffd47 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -561,7 +561,7 @@ def evaluation_date_first_completion(evaluation_id) -> datetime.datetime: return max(date_premiere_note.values()) -def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): +def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"): """Experimental: un tableau indiquant pour chaque évaluation le nombre de jours avant la publication des notes. @@ -638,7 +638,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""", filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()), ) - return tab.make_page(format=format) + return tab.make_page(fmt=fmt) # -------------- VIEWS diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 01c6c536..9a4cee64 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -220,7 +220,7 @@ def get_set_formsemestre_id_dates(start_date, end_date) -> set: def scodoc_table_results( - start_date="", end_date="", types_parcours: list = None, format="html" + start_date="", end_date="", types_parcours: list = None, fmt="html" ): """Page affichant la table des résultats Les dates sont en dd/mm/yyyy (datepicker javascript) @@ -248,8 +248,8 @@ def scodoc_table_results( end_date, "&types_parcours=".join([str(x) for x in types_parcours]), ) - if format != "html": - return tab.make_page(format=format, with_html_headers=False) + if fmt != "html": + return tab.make_page(fmt=fmt, with_html_headers=False) tab_html = tab.html() nb_rows = tab.get_nb_rows() else: diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index d3fa462e..8601586f 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -366,7 +366,7 @@ def table_etud_in_accessible_depts(expnom=None): ) -def search_inscr_etud_by_nip(code_nip, format="json"): +def search_inscr_etud_by_nip(code_nip, fmt="json"): """Recherche multi-departement d'un étudiant par son code NIP Seuls les départements accessibles par l'utilisateur sont cherchés. @@ -408,4 +408,4 @@ def search_inscr_etud_by_nip(code_nip, format="json"): ) tab = GenTable(columns_ids=columns_ids, rows=T) - return tab.make_page(format=format, with_html_headers=False, publish=True) + return tab.make_page(fmt=fmt, with_html_headers=False, publish=True) diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py index 7717e4f9..02c2578d 100644 --- a/app/scodoc/sco_formation_recap.py +++ b/app/scodoc/sco_formation_recap.py @@ -45,7 +45,7 @@ import app.scodoc.sco_utils as scu # ---- Table recap formation -def formation_table_recap(formation_id, format="html") -> Response: +def formation_table_recap(formation_id, fmt="html") -> Response: """Table recapitulant formation.""" T = [] formation = Formation.query.get_or_404(formation_id) @@ -162,7 +162,7 @@ def formation_table_recap(formation_id, format="html") -> Response: preferences=sco_preferences.SemPreferences(), table_id="formation_table_recap", ) - return tab.make_page(format=format, javascripts=["js/formation_recap.js"]) + return tab.make_page(fmt=fmt, javascripts=["js/formation_recap.js"]) def export_recap_formations_annee_scolaire(annee_scolaire): @@ -179,7 +179,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire): formation_ids = {formsemestre.formation.id for formsemestre in formsemestres} for formation_id in formation_ids: formation = db.session.get(Formation, formation_id) - xls = formation_table_recap(formation_id, format="xlsx").data + xls = formation_table_recap(formation_id, fmt="xlsx").data filename = ( scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX ) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index d0c8b688..c58a81d2 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -212,7 +212,7 @@ def formation_export( export_tags=True, export_external_ues=False, export_codes_apo=True, - format=None, + fmt=None, ) -> flask.Response: """Get a formation, with UE, matieres, modules in desired format @@ -224,13 +224,13 @@ def formation_export( export_tags=export_tags, export_external_ues=export_external_ues, export_codes_apo=export_codes_apo, - ac_as_list=format == "xml", + ac_as_list=fmt == "xml", ) filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}" return scu.sendResult( f_dict, name="formation", - format=format, + fmt=fmt, force_outer_xml_tag=False, attached=True, filename=filename, @@ -283,7 +283,7 @@ def _formation_retreive_apc_niveau( def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): """Create a formation from XML representation - (format dumped by formation_export( format='xml' )) + (format dumped by formation_export( fmt='xml' )) XML may contain object (UE, modules) ids: this function returns two dicts mapping these ids to the created ids. @@ -627,7 +627,7 @@ def formation_create_new_version(formation_id, redirect=True): "duplicate formation, with new version number" formation = Formation.query.get_or_404(formation_id) resp = formation_export( - formation_id, export_ids=True, export_external_ues=True, format="xml" + formation_id, export_ids=True, export_external_ues=True, fmt="xml" ) xml_data = resp.get_data(as_text=True) new_id, modules_old2new, ues_old2new = formation_import_xml( diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 40a34a2a..8c77ed6a 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -559,7 +559,7 @@ def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False) -> list[di return sems -def view_formsemestre_by_etape(etape_apo=None, format="html"): +def view_formsemestre_by_etape(etape_apo=None, fmt="html"): """Affiche table des semestres correspondants à l'étape""" if etape_apo: html_title = f"""

      Semestres courants de l'étape {etape_apo}

      """ @@ -575,7 +575,7 @@ def view_formsemestre_by_etape(etape_apo=None, format="html"): """, ) tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "") - return tab.make_page(format=format) + return tab.make_page(fmt=fmt) def sem_has_etape(sem, code_etape): diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 286bcf11..1a0163b4 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -171,6 +171,13 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "enabled": True, "helpmsg": "Tableau de bord du semestre", }, + # { + # "title": "Assiduité du semestre", + # "endpoint": "assiduites.liste_assiduites_formsemestre", + # "args": {"formsemestre_id": formsemestre_id}, + # "enabled": True, + # "helpmsg": "Tableau de l'assiduité et des justificatifs du semestre", + # }, { "title": f"Voir la formation {formation.acronyme} (v{formation.version})", "endpoint": "notes.ue_table", @@ -218,14 +225,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "enabled": True, "helpmsg": "", }, - # TODO: Mettre à jour avec module Assiduités - # { - # "title": "Vérifier absences aux évaluations", - # "endpoint": "notes.formsemestre_check_absences_html", - # "args": {"formsemestre_id": formsemestre_id}, - # "enabled": True, - # "helpmsg": "", - # }, { "title": "Lister tous les enseignants", "endpoint": "notes.formsemestre_enseignants_list", @@ -326,7 +325,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "title": "Exporter table des étudiants", "endpoint": "scolar.groups_view", "args": { - "format": "allxls", + "fmt": "allxls", "group_ids": sco_groups.get_default_group( formsemestre_id, fix_if_missing=True ), @@ -448,7 +447,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "title": "Documents archivés", "endpoint": "notes.formsemestre_list_archives", "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_archives.PVArchive.list_obj_archives(formsemestre_id), + "enabled": sco_archives.PV_ARCHIVER.list_obj_archives(formsemestre_id), }, ] @@ -503,13 +502,10 @@ def retreive_formsemestre_from_request() -> int: group = sco_groups.get_group(args["group_id"]) formsemestre_id = group["formsemestre_id"] elif group_ids: - if group_ids: - if isinstance(group_ids, str): - group_id = group_ids - else: - # prend le semestre du 1er groupe de la liste: - group_id = group_ids[0] - group = sco_groups.get_group(group_id) + if isinstance(group_ids, str): + group_ids = group_ids.split(",") + group_id = group_ids[0] + group = sco_groups.get_group(group_id) formsemestre_id = group["formsemestre_id"] elif "partition_id" in args: partition = sco_groups.get_partition(args["partition_id"]) @@ -788,7 +784,7 @@ def formsemestre_description_table( def formsemestre_description( - formsemestre_id, format="html", with_evals=False, with_parcours=False + formsemestre_id, fmt="html", with_evals=False, with_parcours=False ): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients @@ -808,112 +804,124 @@ def formsemestre_description( >indiquer les parcours BUT """ - return tab.make_page(format=format) + return tab.make_page(fmt=fmt) # genere liste html pour accès aux groupes de ce semestre -def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) - destination = url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - ) - # +def _make_listes_sem(formsemestre: FormSemestre) -> str: + """La section avec les groupes et l'assiduité""" H = [] # pas de menu absences si pas autorise: - if with_absences and not current_user.has_permission(Permission.ScoAbsChange): - with_absences = False - + can_edit_abs = current_user.has_permission(Permission.ScoAbsChange) # H.append( - f"""

      Listes de {formsemestre.titre} - ({formsemestre.mois_debut()} - {formsemestre.mois_fin()})

      """ + f"""

      Groupes et absences de {formsemestre.titre} + ({ + formsemestre.mois_debut()} - {formsemestre.mois_fin() + })

      """ ) - - weekday = datetime.datetime.today().weekday() - try: - if with_absences: - form_abs_tmpl = f""" - - - """ - form_abs_tmpl += f""" - - - - """ - else: - form_abs_tmpl = f""" - - - - """ - except ScoInvalidDateError: # dates incorrectes dans semestres ? - form_abs_tmpl = "" # - H.append('
      ') + H.append('
      ') # Genere liste pour chaque partition (categorie de groupes) - for partition in sco_groups.get_partitions_list(formsemestre.id): - if not partition["partition_name"]: - H.append("

      Tous les étudiants

      ") - else: - H.append("

      Groupes de %(partition_name)s

      " % partition) - partition_is_empty = True - groups = sco_groups.get_partition_groups(partition) + for partition in formsemestre.get_partitions_list(): + groups = partition.groups.all() + effectifs = {g.id: g.get_nb_inscrits() for g in groups} + partition_is_empty = sum(effectifs.values()) == 0 + H.append( + f""" +
      +
      { + 'Groupes de ' + partition.partition_name + if partition.partition_name else + 'Tous les étudiants'} +
      +
      { + "Gestion de l'assiduité" if not partition_is_empty else "" + }
      + """ + ) if groups: - H.append("") for group in groups: - n_members = len(sco_groups.get_group_members(group["group_id"])) + n_members = effectifs[group.id] if n_members == 0: continue # skip empty groups partition_is_empty = False - group["url_etat"] = url_for( - "assiduites.visu_assi_group", - scodoc_dept=g.scodoc_dept, - group_ids=group["id"], - date_debut=formsemestre.date_debut.isoformat(), - date_fin=formsemestre.date_fin.isoformat(), - ) - if group["group_name"]: - group["label"] = "groupe %(group_name)s" % group - else: - group["label"] = "liste" + group_label = f"{group.group_name}" if group.group_name else "liste" H.append( f""" - - - + +
      + """ ) + if can_edit_abs: + H.append( + f""" + + + + + """ + ) - H.append(form_abs_tmpl % group) - - H.append("
      ") - H.append("
      ") + H.append("
      ") # /sem-groups-assi if partition_is_empty: - H.append('

      Aucun groupe peuplé dans cette partition') + H.append( + '

      Aucun groupe peuplé dans cette partition' + ) if formsemestre.can_change_groups(): H.append( f""" (créer)""" ) - H.append("

      ") + H.append("
      ") + H.append("
      ") # /sem-groups-partition + if formsemestre.can_change_groups(): H.append( f"""

      = 0 and not sem.est_sur_une_annee(): @@ -1181,7 +1191,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True): ) # --- LISTE DES ETUDIANTS H += [ - '
      ', + '
      ', _make_listes_sem(formsemestre), "
      ", ] @@ -1230,7 +1240,11 @@ def formsemestre_tableau_modules( mod_descr = "Module " + (mod.titre or "") if mod.is_apc(): coef_descr = ", ".join( - [f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()] + [ + f"{ue.acronyme}: {co}" + for ue, co in mod.ue_coefs_list() + if isinstance(co, float) and co > 0 + ] ) if coef_descr: mod_descr += " Coefs: " + coef_descr diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 6a7b686d..6a4d77e8 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -131,6 +131,7 @@ def get_partition(partition_id): # OBSOLETE def get_partitions_list(formsemestre_id, with_default=True) -> list[dict]: """Liste des partitions pour ce semestre (list of dicts), triées par numéro, avec la partition par défaut en fin de liste. + OBSOLETE: utiliser FormSemestre.get_partitions_list """ partitions = ndb.SimpleDictFetch( """SELECT p.id AS partition_id, p.* @@ -515,7 +516,7 @@ def get_etud_groups_in_partition(partition_id): return R -def formsemestre_partition_list(formsemestre_id, format="xml"): +def formsemestre_partition_list(formsemestre_id, fmt="xml"): """Get partitions and groups in this semestre Supported formats: xml, json """ @@ -523,7 +524,7 @@ def formsemestre_partition_list(formsemestre_id, format="xml"): # Ajoute les groupes for p in partitions: p["group"] = get_partition_groups(p) - return scu.sendResult(partitions, name="partition", format=format) + return scu.sendResult(partitions, name="partition", fmt=fmt) # Encore utilisé par groupmgr.js @@ -1377,20 +1378,18 @@ def group_rename(group_id): return group_set_name(group, tf[2]["group_name"]) -def groups_auto_repartition(partition_id=None): - """Reparti les etudiants dans des groupes dans une partition, en respectant le niveau +def groups_auto_repartition(partition: Partition): + """Réparti les etudiants dans des groupes dans une partition, en respectant le niveau et la mixité. """ - partition: Partition = Partition.query.get_or_404(partition_id) if not partition.groups_editable: raise AccessDenied("Partition non éditable") - formsemestre_id = partition.formsemestre_id formsemestre = partition.formsemestre # renvoie sur page édition partitions et groupes dest_url = url_for( "scolar.partition_editor", scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, + formsemestre_id=formsemestre.id, ) if not formsemestre.can_change_groups(): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") @@ -1409,7 +1408,9 @@ def groups_auto_repartition(partition_id=None): ] H = [ - html_sco_header.sco_header(page_title="Répartition des groupes"), + html_sco_header.sco_header( + page_title="Répartition des groupes", formsemestre_id=formsemestre.id + ), f"""

      Répartition des groupes de {partition.partition_name}

      Semestre {formsemestre.titre_annee()}

      Les groupes existants seront effacés et remplacés par @@ -1455,7 +1456,7 @@ def groups_auto_repartition(partition_id=None): listes = {} for civilite in civilites: listes[civilite] = [ - (_get_prev_moy(x["etudid"], formsemestre_id), x["etudid"]) + (_get_prev_moy(x["etudid"], formsemestre.id), x["etudid"]) for x in identdict.values() if x["civilite"] == civilite ] diff --git a/app/scodoc/sco_groups_exports.py b/app/scodoc/sco_groups_exports.py index d6f63ea0..232f4cc3 100644 --- a/app/scodoc/sco_groups_exports.py +++ b/app/scodoc/sco_groups_exports.py @@ -60,7 +60,7 @@ def groups_list_annotation(group_ids: list[int]) -> list[dict]: return annotations -def groups_export_annotations(group_ids, formsemestre_id=None, format="html"): +def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"): """Les annotations""" groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id @@ -68,7 +68,7 @@ def groups_export_annotations(group_ids, formsemestre_id=None, format="html"): annotations = groups_list_annotation(groups_infos.group_ids) for annotation in annotations: annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M") - if format == "xls": + if fmt == "xls": columns_ids = ("etudid", "nom", "prenom", "date", "comment") else: columns_ids = ("etudid", "nom", "prenom", "date_str", "comment") @@ -93,4 +93,4 @@ def groups_export_annotations(group_ids, formsemestre_id=None, format="html"): html_class="table_leftalign", preferences=sco_preferences.SemPreferences(formsemestre_id), ) - return table.make_page(format=format) + return table.make_page(fmt=fmt) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index b27098ef..179160ba 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -70,7 +70,7 @@ CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS # view: def groups_view( group_ids=(), - format="html", + fmt="html", # Options pour listes: with_codes=0, etat=None, @@ -82,7 +82,7 @@ def groups_view( ): """Affichage des étudiants des groupes indiqués group_ids: liste de group_id - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf """ # Informations sur les groupes à afficher: groups_infos = DisplayedGroupsInfos( @@ -92,10 +92,10 @@ def groups_view( select_all_when_unspecified=True, ) # Formats spéciaux: download direct - if format != "html": + if fmt != "html": return groups_table( groups_infos=groups_infos, - format=format, + fmt=fmt, with_codes=with_codes, etat=etat, with_paiement=with_paiement, @@ -135,7 +135,7 @@ def groups_view( """, groups_table( groups_infos=groups_infos, - format=format, + fmt=fmt, with_codes=with_codes, etat=etat, with_paiement=with_paiement, @@ -324,7 +324,9 @@ class DisplayedGroupsInfos: if not formsemestre_id: raise Exception("missing parameter formsemestre_id or group_ids") if select_all_when_unspecified: - group_ids = [sco_groups.get_default_group(formsemestre_id)] + group_ids = [ + sco_groups.get_default_group(formsemestre_id, fix_if_missing=True) + ] else: # selectionne le premier groupe trouvé, s'il y en a un partition = sco_groups.get_partitions_list( @@ -437,14 +439,14 @@ def groups_table( groups_infos: DisplayedGroupsInfos = None, with_codes=0, etat=None, - format="html", + fmt="html", with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) with_archives=0, # ajoute colonne avec noms fichiers archivés with_annotations=0, with_bourse=0, ): """liste etudiants inscrits dans ce semestre - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape """ from app.scodoc import sco_report @@ -499,12 +501,12 @@ def groups_table( p["partition_id"]: p["partition_name"] for p in groups_infos.partitions } - if format != "html": # ne mentionne l'état que en Excel (style en html) + if fmt != "html": # ne mentionne l'état que en Excel (style en html) columns_ids.append("etat") columns_ids.append("email") columns_ids.append("emailperso") - if format == "moodlecsv": + if fmt == "moodlecsv": columns_ids = ["email", "semestre_groupe"] if with_codes: @@ -579,7 +581,7 @@ def groups_table( else: s = "" - if format == "moodlecsv": + if fmt == "moodlecsv": # de la forme S1-[FI][FA]-groupe.csv if not moodle_groupenames: moodle_groupenames = {"tous"} @@ -612,7 +614,7 @@ def groups_table( preferences=prefs, ) # - if format == "html": + if fmt == "html": amail_inst = [ x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" ] @@ -683,11 +685,11 @@ def groups_table( [ tab.html(), "

      " elif ( - format == "pdf" - or format == "xml" - or format == "json" - or format == "xls" - or format == "moodlecsv" + fmt == "pdf" + or fmt == "xml" + or fmt == "json" + or fmt == "xls" + or fmt == "moodlecsv" ): - if format == "moodlecsv": - format = "csv" - return tab.make_page(format=format) + if fmt == "moodlecsv": + fmt = "csv" + return tab.make_page(fmt=fmt) - elif format == "xlsappel": + elif fmt == "xlsappel": xls = sco_excel.excel_feuille_listeappel( groups_infos.formsemestre, groups_infos.groups_titles, @@ -745,7 +747,7 @@ def groups_table( ) filename = "liste_%s" % groups_infos.groups_filename return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) - elif format == "allxls": + elif fmt == "allxls": # feuille Excel avec toutes les infos etudiants if not groups_infos.members: return "" @@ -825,7 +827,7 @@ def tab_absences_html(groups_infos, etat=None): H.extend( [ - "

      Assiduités

      ", + "

      Assiduité

      ", '", "

      Feuilles

      ", '
        ', - """
      • Feuille d'émargement %s (Excel)
      • """ + """
      • Feuille d'émargement %s (Excel)
      • """ % (groups_infos.base_url, groups_infos.groups_titles), - """
      • Trombinoscope en PDF
      • """ + """
      • Trombinoscope en PDF
      • """ % groups_infos.groups_query_args, - """
      • Trombinoscope en PDF (format "IUT de Tours", beta)
      • """ + """
      • Trombinoscope en PDF (format "IUT de Tours", beta)
      • """ % groups_infos.groups_query_args, - """
      • Feuille relevé absences hebdomadaire (beta)
      • """ + """
      • Feuille relevé absences hebdomadaire (beta)
      • """ % groups_infos.groups_query_args, - """
      • Liste d'appel avec photos
      • """ + """
      • Liste d'appel avec photos
      • """ % groups_infos.groups_query_args, """
      • Liste des annotations
      • """ % groups_infos.groups_query_args, @@ -890,76 +896,38 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None): authuser = current_user if not authuser.has_permission(Permission.ScoAbsChange): return "" - sem = groups_infos.formsemestre - first_monday = sco_cal.ddmmyyyy(sem["date_debut"]).prev_monday() - today_idx = datetime.date.today().weekday() - - FA = [] # formulaire avec menu saisi absences - FA.append( - # TODO-ASSIDUITE et utiliser url_for... (was Absences/SignaleAbsenceGrSemestre) - '
        ' - ) - FA.append('' % sem) - FA.append(groups_infos.get_form_elem()) - if moduleimpl_id: - FA.append( - '' % moduleimpl_id - ) - FA.append('') - - FA.append( - """""" - ) - FA.append("""") - FA.append("
        ") - return "\n".join(FA) + return f""" + + """ -# Ajout Le Havre -# Formulaire saisie absences semaine +# Saisie de l'assiduité par semaine def form_choix_saisie_semaine(groups_infos): authuser = current_user if not authuser.has_permission(Permission.ScoAbsChange): return "" - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) query_args = parse_qs(request.query_string) - moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] - if "head_message" in query_args: - del query_args["head_message"] - destination = "%s?%s" % ( - request.base_url, - urllib.parse.urlencode(query_args, True), - ) - destination = destination.replace( - "%", "%%" - ) # car ici utilisee dans un format string ! - - DateJour = time.strftime("%d/%m/%Y") - datelundi = sco_cal.ddmmyyyy(DateJour).prev_monday() - FA = [] # formulaire avec menu saisie hebdo des absences - # XXX TODO-ASSIDUITE et utiliser un POST - FA.append('
        ') - FA.append('' % datelundi) - FA.append('' % moduleimpl_id) - FA.append('' % destination) - FA.append(groups_infos.get_form_elem()) - FA.append( - '' - ) # XXX - FA.append("
        ") - return "\n".join(FA) + moduleimpl_id = query_args.get("moduleimpl_id", [None])[0] + semaine = datetime.date.today().isocalendar().week + return f""" + + """ def export_groups_as_moodle_csv(formsemestre_id=None): @@ -1004,4 +972,4 @@ def export_groups_as_moodle_csv(formsemestre_id=None): text_with_titles=prefs["moodle_csv_with_headerline"], preferences=prefs, ) - return tab.make_page(format="csv") + return tab.make_page(fmt="csv") diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 00818e54..6ac88dc5 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -60,7 +60,7 @@ from app.scodoc.htmlutils import histogram_notes def do_evaluation_listenotes( - evaluation_id=None, moduleimpl_id=None, format="html" + evaluation_id=None, moduleimpl_id=None, fmt="html" ) -> tuple[str, str]: """ Affichage des notes d'une évaluation (si evaluation_id) @@ -220,7 +220,7 @@ def do_evaluation_listenotes( _make_table_notes( tf[1], evals, - fmt=format, + fmt=fmt, note_sur_20=note_sur_20, anonymous_listing=anonymous_listing, group_ids=group_ids, @@ -424,7 +424,7 @@ def _make_table_notes( key_mgr, note_sur_20, keep_numeric, - format=fmt, + fmt=fmt, ) columns_ids.append(e["evaluation_id"]) # @@ -596,7 +596,7 @@ def _make_table_notes( ) if fmt == "bordereau": fmt = "pdf" - t = tab.make_page(format=fmt, with_html_headers=False) + t = tab.make_page(fmt=fmt, with_html_headers=False) if fmt != "html": return t @@ -622,7 +622,7 @@ def _make_table_notes( histo = histogram_notes(notes) # 2 colonnes: histo, comments C = [ - f'
        Bordereau de Signatures (version PDF)', + f'
        Bordereau de Signatures (version PDF)', "\n", @@ -670,7 +670,7 @@ def _add_eval_columns( K, note_sur_20, keep_numeric, - format="html", + fmt="html", ): """Add eval e""" nb_notes = 0 @@ -774,7 +774,7 @@ def _add_eval_columns( row_coefs[evaluation_id] = "coef. %s" % e["coefficient"] if is_apc: - if format == "html": + if fmt == "html": row_poids[evaluation_id] = _mini_table_eval_ue_poids( evaluation_id, evals_poids, ues ) diff --git a/app/scodoc/sco_lycee.py b/app/scodoc/sco_lycee.py index 4d9cf02f..d3585cd1 100644 --- a/app/scodoc/sco_lycee.py +++ b/app/scodoc/sco_lycee.py @@ -63,7 +63,7 @@ def formsemestre_table_etuds_lycees( ) -def scodoc_table_etuds_lycees(format="html"): +def scodoc_table_etuds_lycees(fmt="html"): """Table avec _tous_ les étudiants des semestres non verrouillés de _tous_ les départements. """ @@ -71,7 +71,7 @@ def scodoc_table_etuds_lycees(format="html"): semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems() etuds = [] try: - for (sem, dept) in semdepts: + for sem, dept in semdepts: app.set_sco_dept(dept.acronym) etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0] finally: @@ -85,8 +85,8 @@ def scodoc_table_etuds_lycees(format="html"): no_links=True, ) tab.base_url = request.base_url - t = tab.make_page(format=format, with_html_headers=False) - if format != "html": + t = tab.make_page(fmt=fmt, with_html_headers=False) + if fmt != "html": return t H = [ html_sco_header.sco_header( @@ -178,7 +178,7 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False) def formsemestre_etuds_lycees( formsemestre_id, - format="html", + fmt="html", only_primo=False, no_grouping=False, ): @@ -191,14 +191,10 @@ def formsemestre_etuds_lycees( tab.base_url += "&only_primo=1" if no_grouping: tab.base_url += "&no_grouping=1" - t = tab.make_page(format=format, with_html_headers=False) - if format != "html": + t = tab.make_page(fmt=fmt, with_html_headers=False) + if fmt != "html": return t - F = [ - sco_report.tsp_form_primo_group( - only_primo, no_grouping, formsemestre_id, format - ) - ] + F = [sco_report.tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)] H = [ html_sco_header.sco_header( page_title=tab.page_title, diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 43b66230..68c04f01 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -299,27 +299,15 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl) if has_expression: H.append( - f""" - + + + """ ) - H.append("""inutilisée dans cette version de ScoDoc""") - if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False): - H.append( - f""" supprimer""" - ) - H.append("""""") - H.append("") else: - H.append( - '") H.append( f"""

        Répartition des notes:

        " + histo + "
        Règle de calcul: - moyenne={modimpl.computation_expr} - """ + """
        Règle de calcul: + inutilisée dans cette version de ScoDoc +
        ' - # règle de calcul standard' - ) + H.append('
        ') H.append("
        Saisie Absences journée + """ + ) + + H.append( + f""" + Saisie Absences hebdo """ ) diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 403b8947..554ce780 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -441,7 +441,7 @@ def ficheEtud(etudid=None): # Fichiers archivés: info["fichiers_archive_htm"] = ( '
        Fichiers associés
        ' - + sco_archives_etud.etud_list_archives_html(etudid) + + sco_archives_etud.etud_list_archives_html(etud) ) # Devenir de l'étudiant: diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 11497367..dd402548 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -27,7 +27,7 @@ _SCO_PERMISSIONS = ( (1 << 13, "ScoAbsChange", "Saisir des absences"), (1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"), # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche - (1 << 15, "ScoEtudChangeAdr", "Changer les addresses d'étudiants"), + (1 << 15, "ScoEtudChangeAdr", "Changer les adresses d'étudiants"), ( 1 << 16, "APIEditGroups", diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 3bc9c2a6..d0cc4ca1 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -383,7 +383,7 @@ class PlacementRunner: self.moduleimpl_data["formsemestre_id"] ), ) - return tab.make_page(format="pdf", with_html_headers=False) + return tab.make_page(fmt="pdf", with_html_headers=False) def _one_header(self, worksheet): cells = [ diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index 77c631e4..428f60ae 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -178,7 +178,7 @@ def _getEtudInfoGroupes(group_ids, etat=None): return etuds -def formsemestre_poursuite_report(formsemestre_id, format="html"): +def formsemestre_poursuite_report(formsemestre_id, fmt="html"): """Table avec informations "poursuite" """ sem = sco_formsemestre.get_formsemestre(formsemestre_id) etuds = _getEtudInfoGroupes([sco_groups.get_default_group(formsemestre_id)]) @@ -230,6 +230,6 @@ def formsemestre_poursuite_report(formsemestre_id, format="html"): title="""

        Poursuite d'études

        """, init_qtip=True, javascripts=["js/etud_info.js"], - format=format, + fmt=fmt, with_html_headers=True, ) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 1acfc5da..2511a2f8 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -609,7 +609,18 @@ class BasePreferences: "category": "abs", }, ), - # Assiduités + # Assiduité + ( + "assi_limit_annee", + { + "initvalue": 1, + "title": "Ne lister que l'assiduités de l'année", + "explanation": "Limite l'affichage des listes d'assiduité et de justificatifs à l'année en cours", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "assi", + }, + ), ( "forcer_module", { @@ -1750,6 +1761,17 @@ class BasePreferences: "category": "bul_mail", }, ), + ( + "bul_mail_list_abs_nb", + { + "initvalue": 10, + "title": "Nombre maximum de dates par catégorie", + "explanation": "dans la liste des absences dans le mail envoyant le bulletin de notes (catégories : abs,abs_just, retard,justificatifs)", + "type": "int", + "size": 3, + "category": "bul_mail", + }, + ), ( "bul_mail_contact_addr", { diff --git a/app/scodoc/sco_pv_forms.py b/app/scodoc/sco_pv_forms.py index 5b7be89e..014a8422 100644 --- a/app/scodoc/sco_pv_forms.py +++ b/app/scodoc/sco_pv_forms.py @@ -206,14 +206,14 @@ def pvjury_table( return lines, titles, columns_ids -def formsemestre_pvjury(formsemestre_id, format="html", publish=True): +def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True): """Page récapitulant les décisions de jury En classique: table spécifique avec les deux semestres pour le DUT En APC/BUT: renvoie vers table recap, en mode jury. """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) is_apc = formsemestre.formation.is_apc() - if format == "html" and is_apc: + if fmt == "html" and is_apc: return redirect( url_for( "notes.formsemestre_recapcomplet", @@ -227,7 +227,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True) if not dpv: - if format == "html": + if fmt == "html": return ( html_sco_header.sco_header() + "

        Aucune information disponible !

        " @@ -239,7 +239,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): formsemestre_id = sem["formsemestre_id"] rows, titles, columns_ids = pvjury_table(dpv) - if format != "html" and format != "pdf": + if fmt != "html" and fmt != "pdf": columns_ids = ["etudid", "code_nip"] + columns_ids tab = GenTable( @@ -255,9 +255,9 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): html_sortable=True, preferences=sco_preferences.SemPreferences(formsemestre_id), ) - if format != "html": + if fmt != "html": return tab.make_page( - format=format, + fmt=fmt, with_html_headers=False, publish=publish, ) diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index 70bc7afe..a558bce1 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -205,7 +205,7 @@ def _results_by_category( bottom_titles["row_title"] = "Total" # ajout titre ligne: - for (cat, l) in zip(categories, C): + for cat, l in zip(categories, C): l["row_title"] = cat if cat is not None else "?" # @@ -274,7 +274,7 @@ def formsemestre_report( return tab -# def formsemestre_report_bacs(formsemestre_id, format='html'): +# def formsemestre_report_bacs(formsemestre_id, fmt='html'): # """ # Tableau sur résultats par type de bac # """ @@ -287,12 +287,12 @@ def formsemestre_report( # title=title) # return tab.make_page( # title = """

        Résultats de %(titreannee)s

        """ % sem, -# format=format, page_title = title) +# fmt=fmt, page_title = title) def formsemestre_report_counts( formsemestre_id: int, - format="html", + fmt="html", category: str = "bac", result: str = None, allkeys: bool = False, @@ -397,10 +397,10 @@ def formsemestre_report_counts( t = tab.make_page( title="""

        Comptes croisés

        """, - format=format, + fmt=fmt, with_html_headers=False, ) - if format != "html": + if fmt != "html": return t H = [ html_sco_header.sco_header(page_title=title), @@ -734,7 +734,7 @@ def table_suivi_cohorte( def formsemestre_suivi_cohorte( formsemestre_id, - format="html", + fmt="html", percent=1, bac="", bacspecialite="", @@ -774,8 +774,8 @@ def formsemestre_suivi_cohorte( ) if only_primo: tab.base_url += "&only_primo=on" - t = tab.make_page(format=format, with_html_headers=False) - if format != "html": + t = tab.make_page(fmt=fmt, with_html_headers=False) + if fmt != "html": return t base_url = request.base_url @@ -1246,7 +1246,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True) return tab -def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, format): +def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt): """Element de formulaire pour choisir si restriction aux primos entrants et groupement par lycees""" F = ["""
        """ % request.base_url] if only_primo: @@ -1268,14 +1268,14 @@ def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, format): F.append( '' % formsemestre_id ) - F.append('' % format) + F.append('' % fmt) F.append("""
        """) return "\n".join(F) def formsemestre_suivi_cursus( formsemestre_id, - format="html", + fmt="html", only_primo=False, no_grouping=False, ): @@ -1290,10 +1290,10 @@ def formsemestre_suivi_cursus( tab.base_url += "&only_primo=1" if no_grouping: tab.base_url += "&no_grouping=1" - t = tab.make_page(format=format, with_html_headers=False) - if format != "html": + t = tab.make_page(fmt=fmt, with_html_headers=False) + if fmt != "html": return t - F = [tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, format)] + F = [tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)] H = [ html_sco_header.sco_header( @@ -1312,7 +1312,7 @@ def formsemestre_suivi_cursus( # ------------- def graph_cursus( formsemestre_id, - format="svg", + fmt="svg", only_primo=False, bac="", # selection sur type de bac bacspecialite="", @@ -1437,7 +1437,7 @@ def graph_cursus( g.add_node(n) g.set("rankdir", "LR") # left to right g.set_fontname("Helvetica") - if format == "svg": + if fmt == "svg": g.set_bgcolor("#fffff0") # ou 'transparent' # titres des semestres: for s in sems.values(): @@ -1489,7 +1489,7 @@ def graph_cursus( n.set("label", "Diplome") # bug si accent (pas compris pourquoi) # Arètes: bubbles = {} # substitue titres pour bulle aides: src_id:dst_id : etud_descr - for (src_id, dst_id) in edges.keys(): + for src_id, dst_id in edges.keys(): e = g.get_edge(src_id, dst_id)[0] e.set("arrowhead", "normal") e.set("arrowsize", 1) @@ -1503,20 +1503,19 @@ def graph_cursus( e.set_URL(f"__xxxetudlist__?{src_id}:{dst_id}") # Genere graphe _, path = tempfile.mkstemp(".gr") - g.write(path=path, format=format) + g.write(path=path, format=fmt) with open(path, "rb") as f: data = f.read() - log("dot generated %d bytes in %s format" % (len(data), format)) + log("dot generated %d bytes in %s format" % (len(data), fmt)) if not data: log("graph.to_string=%s" % g.to_string()) - raise ValueError( - "Erreur lors de la génération du document au format %s" % format - ) + raise ValueError("Erreur lors de la génération du document au format %s" % fmt) os.unlink(path) - if format == "svg": + if fmt == "svg": # dot génère un document XML complet, il faut enlever l'en-tête data_str = data.decode("utf-8") data = "Origine et devenir des étudiants inscrits dans %(titreannee)s""" % sem, """(version pdf""" - % url_for("notes.formsemestre_graph_cursus", format="pdf", **url_kw), + % url_for("notes.formsemestre_graph_cursus", fmt="pdf", **url_kw), """, image PNG)""" - % url_for("notes.formsemestre_graph_cursus", format="png", **url_kw), - """

        """, - """

        Le graphe permet de suivre les étudiants inscrits dans le semestre + % url_for("notes.formsemestre_graph_cursus", fmt="png", **url_kw), + f""" +

        +

        Le graphe permet de suivre les étudiants inscrits dans le semestre sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans - pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants passant - d'un semestre à l'autre (s'il y en a moins de %s, vous pouvez visualiser leurs noms en - passant la souris sur le chiffre). -

        """ - % MAX_ETUD_IN_DESCR, + pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants + passant d'un semestre à l'autre (s'il y en a moins de {MAX_ETUD_IN_DESCR}, vous + pouvez visualiser leurs noms en passant le curseur sur le chiffre). +

        + """, html_sco_header.sco_footer(), ] return "\n".join(H) else: - raise ValueError("invalid format: %s" % format) + raise ValueError(f"invalid format: {fmt}") diff --git a/app/scodoc/sco_report_but.py b/app/scodoc/sco_report_but.py index 5cfaa99e..cb216d4e 100644 --- a/app/scodoc/sco_report_but.py +++ b/app/scodoc/sco_report_but.py @@ -67,7 +67,7 @@ INDICATEUR_NAMES = { } -def formsemestre_but_indicateurs(formsemestre_id: int, format="html"): +def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"): """Page avec tableau indicateurs enquête ADIUT BUT 2022""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) @@ -100,10 +100,10 @@ def formsemestre_but_indicateurs(formsemestre_id: int, format="html"): title = "Indicateurs suivi annuel BUT" t = tab.make_page( title=f"""

        {title}

        """, - format=format, + fmt=fmt, with_html_headers=False, ) - if format != "html": + if fmt != "html": return t H = [ html_sco_header.sco_header(page_title=title), diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index a2c95148..489165bd 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -46,6 +46,7 @@ from app.models import ( Module, ModuleImpl, ScolarNews, + Assiduite, ) from app.models.etudiants import Identite @@ -75,6 +76,8 @@ import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import ModuleType +from flask_sqlalchemy.query import Query + def convert_note_from_string( note: str, @@ -1102,29 +1105,21 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in # Groupes auxquels appartient cet étudiant: e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) - # Information sur absence (tenant compte de la demi-journée) - jour_iso = ( - evaluation.date_debut.date().isoformat() if evaluation.date_debut else "" - ) - warn_abs_lst = [] - if evaluation.is_matin(): - nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True) - nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True) - if nbabs: - if nbabsjust: - warn_abs_lst.append("absent justifié le matin !") - else: - warn_abs_lst.append("absent le matin !") - if evaluation.is_apresmidi(): - nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0) - nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0) - if nbabs: - if nbabsjust: - warn_abs_lst.append("absent justifié l'après-midi !") - else: - warn_abs_lst.append("absent l'après-midi !") + # Information sur absence + warn_abs_lst: str = "" + if evaluation.date_debut is not None and evaluation.date_fin is not None: + assiduites_etud: Query = etud.assiduites.filter( + Assiduite.etat == scu.EtatAssiduite.ABSENT, + Assiduite.date_debut <= evaluation.date_fin, + Assiduite.date_fin >= evaluation.date_debut, + ) + premiere_assi: Assiduite = assiduites_etud.first() + if premiere_assi is not None: + warn_abs_lst: str = ( + f"absent {'justifié' if premiere_assi.est_just else ''}" + ) - e["absinfo"] = '' + " ".join(warn_abs_lst) + " " + e["absinfo"] = '' + warn_abs_lst + " " # Note actuelle de l'étudiant: if etudid in notes_db: diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 49ec35e0..34f940fc 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -306,9 +306,9 @@ class SemSet(dict): H.append("

        ") if self["sem_id"] == 1: - periode = "1re période (S1, S3)" + periode = "1re période (S1, S3, S5)" elif self["sem_id"] == 2: - periode = "2de période (S2, S4)" + periode = "2de période (S2, S4, S6)" else: periode = "non semestrialisée (LP, ...). Incompatible avec BUT." @@ -465,7 +465,7 @@ def do_semset_remove_sem(semset_id, formsemestre_id): # ---------------------------------------- -def semset_page(format="html"): +def semset_page(fmt="html"): """Page avec liste semsets: Table avec : date_debut date_fin titre liste des semestres """ @@ -514,8 +514,8 @@ def semset_page(format="html"): filename="semsets", preferences=sco_preferences.SemPreferences(), ) - if format != "html": - return tab.make_page(format=format) + if fmt != "html": + return tab.make_page(fmt=fmt) page_title = "Ensembles de semestres" H = [ diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index b2203ba9..396df98e 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -66,7 +66,7 @@ def trombino( group_ids=(), # liste des groupes à afficher formsemestre_id=None, # utilisé si pas de groupes selectionné etat=None, - format="html", + fmt="html", dialog_confirmed=False, ): """Trombinoscope""" @@ -78,18 +78,18 @@ def trombino( ) # - if format != "html" and not dialog_confirmed: - ok, dialog = check_local_photos_availability(groups_infos, fmt=format) + if fmt != "html" and not dialog_confirmed: + ok, dialog = check_local_photos_availability(groups_infos, fmt=fmt) if not ok: return dialog - if format == "zip": + if fmt == "zip": return _trombino_zip(groups_infos) - elif format == "pdf": + elif fmt == "pdf": return _trombino_pdf(groups_infos) - elif format == "pdflist": + elif fmt == "pdflist": return _listeappel_photos_pdf(groups_infos) - elif format == "doc": + elif fmt == "doc": return sco_trombino_doc.trombino_doc(groups_infos) else: raise Exception("invalid format") @@ -110,7 +110,7 @@ def trombino_html(groups_infos): { "title": "Obtenir archive Zip des photos", "endpoint": "scolar.trombino", - "args": {"group_ids": groups_infos.group_ids, "format": "zip"}, + "args": {"group_ids": groups_infos.group_ids, "fmt": "zip"}, }, { "title": "Recopier les photos depuis le portail", @@ -176,10 +176,10 @@ def trombino_html(groups_infos): H.append( f"""
        Version PDF + fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF    Version doc + fmt='doc', group_ids=groups_infos.group_ids)}">Version doc
        """ ) return "\n".join(H) @@ -198,14 +198,14 @@ def check_local_photos_availability(groups_infos, fmt=""): if not sco_photos.etud_photo_is_local(t["photo_filename"]): nb_missing += 1 if nb_missing > 0: - parameters = {"group_ids": groups_infos.group_ids, "format": fmt} + parameters = {"group_ids": groups_infos.group_ids, "fmt": fmt} return ( False, scu.confirm_dialog( f"""

        Attention: {nb_missing} photos ne sont pas disponibles et ne peuvent pas être exportées.

        Vous pouvez exporter seulement les photos existantes""", dest_url="trombino", OK="Exporter seulement les photos existantes", diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index faaf1f51..61b84c0c 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -173,7 +173,7 @@ def evaluation_list_operations(evaluation_id): return tab.make_page() -def formsemestre_list_saisies_notes(formsemestre_id, format="html"): +def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"): """Table listant toutes les opérations de saisies de notes, dans toutes les évaluations du semestre. """ @@ -194,7 +194,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"): {"formsemestre_id": formsemestre_id}, ) # Formate les notes - keep_numeric = format in scu.FORMATS_NUMERIQUES + keep_numeric = fmt in scu.FORMATS_NUMERIQUES for row in rows: row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric) row["date_evaluation"] = ( @@ -242,7 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"): base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id), origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "", ) - return tab.make_page(format=format) + return tab.make_page(fmt=fmt) def get_note_history(evaluation_id, etudid, fmt=""): diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index 63ae2aae..8492dcff 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -240,7 +240,7 @@ def list_users( preferences=sco_preferences.SemPreferences(), ) - return tab.make_page(format=fmt, with_html_headers=False) + return tab.make_page(fmt=fmt, with_html_headers=False) def get_users_count(dept=None) -> int: diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index ee8417f3..5baba634 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -237,7 +237,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: new_date: datetime.datetime = date if new_date.tzinfo is None: try: - new_date = timezone("Europe/Paris").localize(date) + new_date = TIME_ZONE.localize(date) except OverflowError: new_date = timezone("UTC").localize(date) return new_date @@ -670,8 +670,8 @@ def AbsencesURL(): def AssiduitesURL(): """URL of Assiduités""" - return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") + return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[ + : -len("/BilanDept") ] @@ -879,10 +879,10 @@ DB_MIN_INT = -(1 << 31) DB_MAX_INT = (1 << 31) - 1 -def bul_filename_old(sem: dict, etud: dict, format): +def bul_filename_old(sem: dict, etud: dict, fmt): """Build a filename for this bulletin""" dt = time.strftime("%Y-%m-%d") - filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}" + filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{fmt}" filename = make_filename(filename) return filename @@ -952,15 +952,15 @@ def sendXML( def sendResult( data, name=None, - format=None, + fmt=None, force_outer_xml_tag=True, attached=False, quote_xml=False, filename=None, ): - if (format is None) or (format == "html"): + if (fmt is None) or (fmt == "html"): return data - elif format == "xml": # name is outer tagname + elif fmt == "xml": # name is outer tagname return sendXML( data, tagname=name, @@ -969,10 +969,10 @@ def sendResult( quote=quote_xml, filename=filename, ) - elif format == "json": + elif fmt == "json": return sendJSON(data, attached=attached, filename=filename) else: - raise ValueError("invalid format: %s" % format) + raise ValueError(f"invalid format: {fmt}") def send_file(data, filename="", suffix="", mime=None, attached=None): @@ -1035,9 +1035,7 @@ def get_request_args(): def json_error(status_code, message=None) -> Response: - """Simple JSON for errors. - If as-response, returns Flask's Response. Otherwise returns a dict. - """ + """Simple JSON for errors.""" payload = { "error": HTTP_STATUS_CODES.get(status_code, "Unknown error"), "status": status_code, diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index 10031d7d..ddee6ed8 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -136,6 +136,8 @@ flex-direction: column; align-items: flex-start; margin: 0 5%; + + cursor: pointer; } .etud_row.def .nom::after, @@ -268,6 +270,7 @@ background-size: cover; } + .rbtn.present::before { background-image: url(../icons/present.svg); } @@ -285,8 +288,8 @@ } .rbtn:checked:before { - outline: 3px solid #7059FF; - border-radius: 5px; + outline: 5px solid #7059FF; + border-radius: 50%; } .rbtn:focus { @@ -541,6 +544,17 @@ background-image: url(../icons/filter.svg); } +.download { + background-image: url(../icons/download.svg); +} + +.iconline { + display: flex; + justify-content: flex-start; + gap: min(2%, 15px); + align-items: center; +} + [name='destroyFile'] { -webkit-appearance: none; appearance: none; diff --git a/app/static/css/bulletin_court.css b/app/static/css/bulletin_court.css index 6e16b6b7..614a3321 100644 --- a/app/static/css/bulletin_court.css +++ b/app/static/css/bulletin_court.css @@ -5,11 +5,17 @@ } } +div.but_bul_court_links { + margin-left: 16px; + margin-bottom: 16px; +} + div.but_bul_court { - width: 17cm; + /* width: 17cm; */ display: grid; - grid-template-columns: 6cm 11cm; font-size: 11pt; + grid-template-columns: 6cm 11cm; + margin-left: 16px; } #infos_etudiant { diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index 1f7a492c..25a31c97 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -28,7 +28,7 @@ main { ; --couleurSurlignage: rgba(255, 253, 110, 0.49); max-width: 1000px; - margin: auto; + margin-left: 16px; display: none; } diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 9312a5d6..baf81050 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -985,17 +985,6 @@ span.linktitresem a:visited { color: red; } -.listegroupelink a:link { - color: blue; -} - -.listegroupelink a:visited { - color: blue; -} - -.listegroupelink a:hover { - color: red; -} a.stdlink, a.stdlink:visited { @@ -1792,10 +1781,6 @@ td.formsemestre_status_inscrits { text-align: center; } -div.formsemestre_status button { - margin-left: 12px;; -} - td.rcp_titre_sem a.jury_link { margin-left: 8px; color: red; @@ -1857,15 +1842,54 @@ ul.ue_inscr_list li.etud { margin-bottom: 5px; } -#grouplists h4 { +.sem-groups-abs { + background-color: rgb(137,137,137); + border-radius: 16px; + padding: 16px; + width: fit-content; +} +.sem-groups-abs h4 { font-style: italic; margin-bottom: 0px; margin-top: 5px; } -#grouplists table { - /*border: 1px solid black;*/ - border-spacing: 1px; +.sem-groups-partition-titre { + margin-left: 4px; + font-size: 110%; +} +.sem-groups-partition { + background-color: rgb(213,203,183); + border-radius: 12px; + margin-bottom: 8px; + padding: 12px; + display: grid; + grid-template-columns: 240px auto; +} + +.sem-groups-list, .sem-groups-assi { + background-color: white; + border-radius: 6px; + margin: 4px; +} + +.sem-groups-list > div { + margin: 4px; +} +.sem-groups-assi > div { + margin: 6px 8px 6px 8px; +} + +.sem-groups-assi { + display: flex; + flex-direction: row; + justify-content: flex-start; + flex-wrap: wrap; + align-items: center; +} + +.sem-groups-none { + grid-column: 1 / span 2; } /* Tableau de bord module */ @@ -3077,7 +3101,7 @@ div.bul_foot { border-radius: 16px; border: 1px solid #AAA; padding: 16px 32px; - margin: auto; + margin-left: 16px; } div.bull_appreciations { @@ -3182,6 +3206,9 @@ table.abs_form_table tr:hover td { border: 1px solid red; } +.ul_abs button { + margin-bottom: 6px; +} /* ----- Formulator ------- */ ul.tf-msg { diff --git a/app/static/icons/download.svg b/app/static/icons/download.svg new file mode 100644 index 00000000..1ee311b5 --- /dev/null +++ b/app/static/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 2ee935ef..ad3d52a1 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -162,6 +162,7 @@ function uniqueCheckBox(box) { * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function sync_get(path, success, errors) { + console.log("sync_get " + path); $.ajax({ async: false, type: "GET", @@ -177,6 +178,7 @@ function sync_get(path, success, errors) { * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function async_get(path, success, errors) { + console.log("async_get " + path); $.ajax({ async: true, type: "GET", @@ -193,6 +195,7 @@ function async_get(path, success, errors) { * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function sync_post(path, data, success, errors) { + console.log("sync_post " + path); $.ajax({ async: false, type: "POST", @@ -210,6 +213,7 @@ function sync_post(path, data, success, errors) { * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function async_post(path, data, success, errors) { + console.log("sync_post " + path); return $.ajax({ async: true, type: "POST", @@ -577,7 +581,7 @@ function updateDate() { return true; } else { const att = document.createTextNode( - "Le jour sélectionné n'est pas un jour travaillé." + `Le jour sélectionné (${formatDate(date)}) n'est pas un jour travaillé.` ); openAlertModal("Erreur", att, "", "crimson"); dateInput.value = dateInput.getAttribute("value"); @@ -611,7 +615,9 @@ function setupDate(onchange = null) { datestr.addEventListener("click", () => { if (!input.disabled) { - input.showPicker(); + try { + input.showPicker(); + } catch {} } }); @@ -809,13 +815,10 @@ function numberTimeToDate(nb) { * - du semestre * - de la date courant et du jour précédent. * @param {boolean} clear vidage de l'objet "assiduites" ou non - * @returns {object} l'objets Assiduités { : [,]} + * @returns {object} l'objet Assiduités { : [,]} */ -function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { +function getAssiduitesFromEtuds(clear, deb, fin) { const etudIds = Object.keys(etuds).join(","); - const formsemestre_id = has_formsemestre - ? `formsemestre_id=${getFormSemestreId()}&` - : ""; const date_debut = deb ? deb : toIsoString(getPrevDate()); const date_fin = fin ? fin : toIsoString(getNextDate()); @@ -826,7 +829,7 @@ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { const url_api = getUrl() + - `/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`; + `/api/assiduites/group/query?date_debut=${date_debut}&date_fin=${date_fin}&etudids=${etudIds}`; sync_get(url_api, (data, status) => { if (status === "success") { const dataKeys = Object.keys(data); @@ -924,14 +927,11 @@ function deleteAssiduite(assiduite_id) { function hasModuleImpl(assiduite) { if (assiduite.moduleimpl_id != null) return true; - if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object && - "module" in assiduite.external_data - ) - return true; - - return false; + return ( + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null && + assiduite.external_data.hasOwnProperty("module") + ); } /** @@ -942,6 +942,15 @@ function hasModuleImpl(assiduite) { * TODO : Rendre asynchrone */ function editAssiduite(assiduite_id, etat, assi) { + if (assi.length != 1 || !assi[0].hasOwnProperty("assiduite_id")) { + const html = ` +

        Aucune assiduité n'a pû être éditée

        + `; + const div = document.createElement("div"); + div.innerHTML = html; + openAlertModal("Erreur", div); + return; + } let assiduite = { etat: etat, external_data: assi ? assi.external_data : null, @@ -1057,16 +1066,13 @@ function getAssiduiteValue(field) { * Mise à jour des assiduités d'un étudiant * @param {String | Number} etudid identifiant de l'étudiant */ -function actualizeEtudAssiduite(etudid, has_formsemestre = true) { - const formsemestre_id = has_formsemestre - ? `formsemestre_id=${getFormSemestreId()}&` - : ""; +function actualizeEtudAssiduite(etudid) { const date_debut = toIsoString(getPrevDate()); const date_fin = toIsoString(getNextDate()); const url_api = getUrl() + - `/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`; + `/api/assiduites/${etudid}/query?date_debut=${date_debut}&date_fin=${date_fin}`; sync_get(url_api, (data, status) => { if (status === "success") { assiduites[etudid] = data; @@ -1074,8 +1080,22 @@ function actualizeEtudAssiduite(etudid, has_formsemestre = true) { }); } -function getAllAssiduitesFromEtud(etudid, action) { - const url_api = getUrl() + `/api/assiduites/${etudid}`; +function getAllAssiduitesFromEtud( + etudid, + action, + order = false, + justifs = false, + courant = false +) { + const url_api = + getUrl() + + `/api/assiduites/${etudid}${ + order + ? "/query?order%°" + .replace("%", justifs ? "&with_justifs" : "") + .replace("°", courant ? "&courant" : "") + : "" + }`; $.ajax({ async: true, @@ -1136,9 +1156,7 @@ function assiduiteAction(element) { done = editAssiduite( assiduite_id, etat, - assiduites[etudid].reduce((a) => { - if (a.assiduite_id == assiduite_id) return a; - }) + assiduites[etudid].filter((a) => a.assiduite_id == assiduite_id) ); } break; @@ -1249,12 +1267,10 @@ function generateEtudRow( - +
        @@ -1331,7 +1347,7 @@ function insertEtudRow(etud, index, output = false) { * @param {String | Number} etudid l'identifiant de l'étudiant */ function actualizeEtud(etudid) { - actualizeEtudAssiduite(etudid, !isSingleEtud()); + actualizeEtudAssiduite(etudid); //Actualize row const etudHolder = document.querySelector(".etud_holder"); const ancient_row = document.getElementById(`etud_row_${etudid}`); @@ -1412,10 +1428,10 @@ function setModuleImplId(assiduite, module = null) { const moduleimpl = module == null ? getModuleImplId() : module; if (moduleimpl === "autre") { if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null ) { - if ("module" in assiduite.external_data) { + if (assiduite.external_data.hasOwnProperty("module")) { assiduite.external_data.module = "Autre"; } else { assiduite["external_data"] = { module: "Autre" }; @@ -1427,10 +1443,10 @@ function setModuleImplId(assiduite, module = null) { } else { assiduite["moduleimpl_id"] = moduleimpl; if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null ) { - if ("module" in assiduite.external_data) { + if (assiduite.external_data.hasOwnProperty("module")) { delete assiduite.external_data.module; } } @@ -1482,9 +1498,9 @@ function getCurrentAssiduiteModuleImplId() { let mod = currentAssiduites[0].moduleimpl_id; if ( mod == null && - "external_data" in currentAssiduites[0] && - currentAssiduites[0].external_data instanceof Object && - "module" in currentAssiduites[0].external_data + currentAssiduites[0].hasOwnProperty("external_data") && + currentAssiduites[0].external_data != null && + currentAssiduites[0].external_data.hasOwnProperty("module") ) { mod = currentAssiduites[0].external_data.module; } @@ -1567,20 +1583,18 @@ function fastJustify(assiduite) { //créer justificatif const justif = { - date_debut: new moment.tz(assiduite.date_debut, TIMEZONE) - .add(1, "s") - .format(), - date_fin: new moment.tz(assiduite.date_fin, TIMEZONE) - .subtract(1, "s") - .format(), + date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(), + date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(), raison: raison, etat: etat, }; createJustificatif(justif); - // justifyAssiduite(assiduite.assiduite_id, true); generateAllEtudRow(); + try { + loadAll(); + } catch {} }; const content = document.createElement("fieldset"); @@ -1643,8 +1657,17 @@ function createJustificatif(justif, success = () => {}) { }); } -function getAllJustificatifsFromEtud(etudid, action) { - const url_api = getUrl() + `/api/justificatifs/${etudid}`; +function getAllJustificatifsFromEtud( + etudid, + action, + order = false, + courant = false +) { + const url_api = + getUrl() + + `/api/justificatifs/${etudid}${ + order ? "/query?order°".replace("°", courant ? "&courant" : "") : "" + }`; $.ajax({ async: true, type: "GET", @@ -1696,9 +1719,9 @@ function getModuleImpl(assiduite) { if (id == null || id == undefined) { if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object && - "module" in assiduite.external_data + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null && + assiduite.external_data.hasOwnProperty("module") ) { return assiduite.external_data.module; } else { @@ -1724,10 +1747,12 @@ function getModuleImpl(assiduite) { } function getUser(obj) { - if ("external_data" in obj && obj.external_data != null) { - if ("enseignant" in obj.external_data) { - return obj.external_data.enseignant; - } + if ( + obj.hasOwnProperty("external_data") && + obj.external_data != null && + obj.external_data.hasOwnProperty("enseignant") + ) { + return obj.external_data.enseignant; } return obj.user_id; diff --git a/app/static/js/etud_debouche.js b/app/static/js/etud_debouche.js index fbcdd197..d24a2b8d 100644 --- a/app/static/js/etud_debouche.js +++ b/app/static/js/etud_debouche.js @@ -4,135 +4,165 @@ // console.log('etud_debouche.js loaded'); $(function () { - display_itemsuivis(false); + display_itemsuivis(false); }); - function display_itemsuivis(active) { - var etudid = $('div#fichedebouche').data("etudid"); - var readonly = $('div#fichedebouche').data('readonly'); // present ro interface + var etudid = $("div#fichedebouche").data("etudid"); + var readonly = $("div#fichedebouche").data("readonly"); // present ro interface - if (!readonly) { - $('#adddebouchelink').off("click").click(function (e) { - e.preventDefault(); - $.post(SCO_URL + "/itemsuivi_create", { etudid: etudid, format: 'json' }).done(item_insert_new); + if (!readonly) { + $("#adddebouchelink") + .off("click") + .click(function (e) { + e.preventDefault(); + $.post(SCO_URL + "/itemsuivi_create", { + etudid: etudid, + fmt: "json", + }).done(item_insert_new); - return false; - }); + return false; + }); + } + // add existing items + $.get( + SCO_URL + "/itemsuivi_list_etud", + { etudid: etudid, fmt: "json" }, + function (L) { + for (var i in L) { + item_insert( + L[i]["itemsuivi_id"], + L[i]["item_date"], + L[i]["situation"], + L[i]["tags"], + readonly + ); + } } - // add existing items - $.get(SCO_URL + "/itemsuivi_list_etud", { etudid: etudid, format: 'json' }, function (L) { - for (var i in L) { - item_insert(L[i]['itemsuivi_id'], L[i]['item_date'], L[i]['situation'], L[i]['tags'], readonly); - } - }); + ); - $("div#fichedebouche").accordion({ - heightStyle: "content", - collapsible: true, - active: active, - }); + $("div#fichedebouche").accordion({ + heightStyle: "content", + collapsible: true, + active: active, + }); } function item_insert_new(it) { - item_insert(it.itemsuivi_id, it.item_date, it.situation, '', false); + item_insert(it.itemsuivi_id, it.item_date, it.situation, "", false); } function item_insert(itemsuivi_id, item_date, situation, tags, readonly) { - if (item_date === undefined) - item_date = Date2DMY(new Date()); - if (situation === undefined) - situation = ''; - if (tags === undefined) - tags = ''; + if (item_date === undefined) item_date = Date2DMY(new Date()); + if (situation === undefined) situation = ""; + if (tags === undefined) tags = ""; - var nodes = item_nodes(itemsuivi_id, item_date, situation, tags, readonly); - // insert just before last li: - if ($('ul.listdebouches li.adddebouche').length > 0) { - $('ul.listdebouches').children(':last').before(nodes); - } else { - // mode readonly, pas de li "ajouter" - $('ul.listdebouches').append(nodes); - } -}; + var nodes = item_nodes(itemsuivi_id, item_date, situation, tags, readonly); + // insert just before last li: + if ($("ul.listdebouches li.adddebouche").length > 0) { + $("ul.listdebouches").children(":last").before(nodes); + } else { + // mode readonly, pas de li "ajouter" + $("ul.listdebouches").append(nodes); + } +} function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) { - // console.log('item_nodes: itemsuivi_id=' + itemsuivi_id); - var sel_mois = 'Situation à la date du supprimer cet item'; + // console.log('item_nodes: itemsuivi_id=' + itemsuivi_id); + var sel_mois = + 'Situation à la date du supprimer cet item'; - var h = sel_mois; - // situation - h += '
        ' + situation + '
        '; - // tags: - h += '
        '; + var h = sel_mois; + // situation + h += + '
        ' + + situation + + "
        "; + // tags: + h += + '
        "; - var nodes = $($.parseHTML('
      • ' + h + '
      • ')); - var dp = nodes.find('.itemsuividatepicker'); - dp.blur(function (e) { - var date = this.value; - // console.log('selected text: ' + date); - $.post(SCO_URL + "/itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id }); + var nodes = $($.parseHTML('
      • ' + h + "
      • ")); + var dp = nodes.find(".itemsuividatepicker"); + dp.blur(function (e) { + var date = this.value; + // console.log('selected text: ' + date); + $.post(SCO_URL + "/itemsuivi_set_date", { + item_date: date, + itemsuivi_id: itemsuivi_id, }); - dp.datepicker({ - onSelect: function (date, instance) { - // console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id); - $.post(SCO_URL + "/itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id }); - }, - showOn: 'button', - buttonImage: '/ScoDoc/static/icons/calendar_img.png', - buttonImageOnly: true, - dateFormat: 'dd/mm/yy', - duration: 'fast', - disabled: readonly - }); - dp.datepicker('option', $.extend({ showMonthAfterYear: false }, - $.datepicker.regional['fr'])); + }); + dp.datepicker({ + onSelect: function (date, instance) { + // console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id); + $.post(SCO_URL + "/itemsuivi_set_date", { + item_date: date, + itemsuivi_id: itemsuivi_id, + }); + }, + showOn: "button", + buttonImage: "/ScoDoc/static/icons/calendar_img.png", + buttonImageOnly: true, + dateFormat: "dd/mm/yy", + duration: "fast", + disabled: readonly, + }); + dp.datepicker( + "option", + $.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"]) + ); - if (readonly) { - // show tags read-only - readOnlyTags(nodes.find('.itemsuivi_tag_editor')); - } - else { - // bind tag editor - nodes.find('.itemsuivi_tag_editor').tagEditor({ - initialTags: '', - placeholder: 'Tags...', - onChange: function (field, editor, tags) { - $.post('itemsuivi_tag_set', - { - itemsuivi_id: itemsuivi_id, - taglist: tags.join() - }); - }, - autocomplete: { - delay: 200, // ms before suggest - position: { collision: 'flip' }, // automatic menu position up/down - source: "itemsuivi_tag_search" - }, + if (readonly) { + // show tags read-only + readOnlyTags(nodes.find(".itemsuivi_tag_editor")); + } else { + // bind tag editor + nodes.find(".itemsuivi_tag_editor").tagEditor({ + initialTags: "", + placeholder: "Tags...", + onChange: function (field, editor, tags) { + $.post("itemsuivi_tag_set", { + itemsuivi_id: itemsuivi_id, + taglist: tags.join(), }); + }, + autocomplete: { + delay: 200, // ms before suggest + position: { collision: "flip" }, // automatic menu position up/down + source: "itemsuivi_tag_search", + }, + }); - // bind inplace editor - nodes.find('div.itemsituation').jinplace(); - } + // bind inplace editor + nodes.find("div.itemsituation").jinplace(); + } - return nodes; -}; + return nodes; +} function Date2DMY(date) { - var year = date.getFullYear(); + var year = date.getFullYear(); - var month = (1 + date.getMonth()).toString(); - month = month.length > 1 ? month : '0' + month; + var month = (1 + date.getMonth()).toString(); + month = month.length > 1 ? month : "0" + month; - var day = date.getDate().toString(); - day = day.length > 1 ? day : '0' + day; + var day = date.getDate().toString(); + day = day.length > 1 ? day : "0" + day; - return day + '/' + month + '/' + year; + return day + "/" + month + "/" + year; } function itemsuivi_suppress(itemsuivi_id) { - $.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id }); - // Clear items and rebuild: - $("ul.listdebouches li.itemsuivi").remove(); - display_itemsuivis(0); + $.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id }); + // Clear items and rebuild: + $("ul.listdebouches li.itemsuivi").remove(); + display_itemsuivis(0); } diff --git a/app/static/js/radar_bulletin.js b/app/static/js/radar_bulletin.js index 2a60081e..43fe13dc 100644 --- a/app/static/js/radar_bulletin.js +++ b/app/static/js/radar_bulletin.js @@ -6,12 +6,11 @@ // Ce code utilise d3.js $().ready(function () { - var etudid = $("#etudid")[0].value; - var formsemestre_id = $("#formsemestre_id")[0].value; - get_notes_and_draw(formsemestre_id, etudid); + var etudid = $("#etudid")[0].value; + var formsemestre_id = $("#formsemestre_id")[0].value; + get_notes_and_draw(formsemestre_id, etudid); }); - var WIDTH = 460; // taille du canvas SVG var HEIGHT = WIDTH; var CX = WIDTH / 2; // coordonnees centre du cercle @@ -24,258 +23,314 @@ var R_AXIS_TICS = [4, 6, 8, 10, 12, 14, 16, 18, 20]; var NB_TICS = R_TICS.length; function get_notes_and_draw(formsemestre_id, etudid) { - console.log("get_notes(" + formsemestre_id + ", " + etudid + " )"); - /* Recupère le bulletin de note et extrait tableau de notes */ - /* + console.log("get_notes(" + formsemestre_id + ", " + etudid + " )"); + /* Recupère le bulletin de note et extrait tableau de notes */ + /* var notes = [ { 'module' : 'E1', 'note' : 13, 'moy' : 16 }, ]; */ - var query = SCO_URL + "/Notes/formsemestre_bulletinetud?formsemestre_id=" + formsemestre_id + "&etudid=" + etudid + "&format=json&version=selectedevals&force_publishing=1" + var query = + SCO_URL + + "/Notes/formsemestre_bulletinetud?formsemestre_id=" + + formsemestre_id + + "&etudid=" + + etudid + + "&fmt=json&version=selectedevals&force_publishing=1"; - $.get(query, '', function (bul) { - var notes = []; - bul.ue.forEach( - function (ue, i, ues) { - ue['module'].forEach(function (m, i) { - notes.push({ - 'code': m['code'], - 'titre': m['titre'], - 'note': m['note']['value'], - 'moy': m['note']['moy'] - }); - }); - }); - draw_radar(notes); + $.get(query, "", function (bul) { + var notes = []; + bul.ue.forEach(function (ue, i, ues) { + ue["module"].forEach(function (m, i) { + notes.push({ + code: m["code"], + titre: m["titre"], + note: m["note"]["value"], + moy: m["note"]["moy"], + }); + }); }); + draw_radar(notes); + }); } function draw_radar(notes) { - /* Calcul coordonnées des éléments */ - var nmod = notes.length; - var angle = 2 * Math.PI / nmod; + /* Calcul coordonnées des éléments */ + var nmod = notes.length; + var angle = (2 * Math.PI) / nmod; - for (var i = 0; i < notes.length; i++) { - var d = notes[i]; - var cx = Math.sin(i * angle); - var cy = - Math.cos(i * angle); - d["x_v"] = CX + RR * d.note / 20 * cx; - d["y_v"] = CY + RR * d.note / 20 * cy; - d["x_moy"] = CX + RR * d.moy / 20 * cx; - d["y_moy"] = CY + RR * d.moy / 20 * cy; - d["x_20"] = CX + RR * cx; - d["y_20"] = CY + RR * cy; - d["x_label"] = CX + (RR + 25) * cx - 10 - d["y_label"] = CY + (RR + 25) * cy + 10; - d["tics"] = []; - // Coords des tics sur chaque axe - for (var j = 0; j < NB_TICS; j++) { - var r = R_TICS[j] / 20 * RR; - d["tics"][j] = { "x": CX + r * cx, "y": CY + r * cy }; - } - } - - var notes_circ = notes.slice(0); - notes_circ.push(notes[0]) - var notes_circ_valid = notes_circ.filter(function (e, i, a) { return e.note != 'NA' && e.note != '-'; }); - var notes_valid = notes.filter(function (e, i, a) { return e.note != 'NA' && e.note != '-'; }) - - /* Crée l'élément SVG */ - g = d3.select("#radar_bulletin").append("svg") - .attr("class", "radar") - .attr("width", WIDTH + 100) - .attr("height", HEIGHT); - - /* Centre */ - g.append("circle").attr("cy", CY) - .attr("cx", CX) - .attr("r", 2) - .attr("class", "radar_center_mark"); - - /* Lignes "tics" */ + for (var i = 0; i < notes.length; i++) { + var d = notes[i]; + var cx = Math.sin(i * angle); + var cy = -Math.cos(i * angle); + d["x_v"] = CX + ((RR * d.note) / 20) * cx; + d["y_v"] = CY + ((RR * d.note) / 20) * cy; + d["x_moy"] = CX + ((RR * d.moy) / 20) * cx; + d["y_moy"] = CY + ((RR * d.moy) / 20) * cy; + d["x_20"] = CX + RR * cx; + d["y_20"] = CY + RR * cy; + d["x_label"] = CX + (RR + 25) * cx - 10; + d["y_label"] = CY + (RR + 25) * cy + 10; + d["tics"] = []; + // Coords des tics sur chaque axe for (var j = 0; j < NB_TICS; j++) { - var ligne_tics = d3.svg.line() - .x(function (d) { return d["tics"][j]["x"]; }) - .y(function (d) { return d["tics"][j]["y"]; }); - g.append("svg:path") - .attr("class", "radar_disk_tic") - .attr("id", "radar_disk_tic_" + R_TICS[j]) - .attr("d", ligne_tics(notes_circ)); + var r = (R_TICS[j] / 20) * RR; + d["tics"][j] = { x: CX + r * cx, y: CY + r * cy }; } + } - /* Lignes radiales pour chaque module */ - g.selectAll("radar_rad") - .data(notes) - .enter().append("line") - .attr("x1", CX) - .attr("y1", CY) - .attr("x2", function (d) { return d["x_20"]; }) - .attr("y2", function (d) { return d["y_20"]; }) - .attr("class", "radarrad"); + var notes_circ = notes.slice(0); + notes_circ.push(notes[0]); + var notes_circ_valid = notes_circ.filter(function (e, i, a) { + return e.note != "NA" && e.note != "-"; + }); + var notes_valid = notes.filter(function (e, i, a) { + return e.note != "NA" && e.note != "-"; + }); + /* Crée l'élément SVG */ + g = d3 + .select("#radar_bulletin") + .append("svg") + .attr("class", "radar") + .attr("width", WIDTH + 100) + .attr("height", HEIGHT); - /* Lignes entre notes */ - var ligne = d3.svg.line() - .x(function (d) { return d["x_v"]; }) - .y(function (d) { return d["y_v"]; }); + /* Centre */ + g.append("circle") + .attr("cy", CY) + .attr("cx", CX) + .attr("r", 2) + .attr("class", "radar_center_mark"); + /* Lignes "tics" */ + for (var j = 0; j < NB_TICS; j++) { + var ligne_tics = d3.svg + .line() + .x(function (d) { + return d["tics"][j]["x"]; + }) + .y(function (d) { + return d["tics"][j]["y"]; + }); g.append("svg:path") - .attr("class", "radarnoteslines") - .attr("d", ligne(notes_circ_valid)); + .attr("class", "radar_disk_tic") + .attr("id", "radar_disk_tic_" + R_TICS[j]) + .attr("d", ligne_tics(notes_circ)); + } - var ligne_moy = d3.svg.line() - .x(function (d) { return d["x_moy"]; }) - .y(function (d) { return d["y_moy"]; }) + /* Lignes radiales pour chaque module */ + g.selectAll("radar_rad") + .data(notes) + .enter() + .append("line") + .attr("x1", CX) + .attr("y1", CY) + .attr("x2", function (d) { + return d["x_20"]; + }) + .attr("y2", function (d) { + return d["y_20"]; + }) + .attr("class", "radarrad"); - g.append("svg:path") - .attr("class", "radarmoylines") - .attr("d", ligne_moy(notes_circ_valid)); + /* Lignes entre notes */ + var ligne = d3.svg + .line() + .x(function (d) { + return d["x_v"]; + }) + .y(function (d) { + return d["y_v"]; + }); - /* Points (notes) */ - g.selectAll("circle1") - .data(notes_valid) - .enter().append("circle") - .attr("cx", function (d) { return d["x_v"]; }) - .attr("cy", function (d) { return d["y_v"]; }) - .attr("r", function (x, i) { return 3; }) - .style("stroke-width", 1) - .style("stroke", "black") - .style("fill", "blue") - .on("mouseover", function (d) { - var rwidth = 310; - var x = d["x_v"]; - if ((x - CX) < 0) { - x = x + 5; - if (x + rwidth + 12 > WIDTH) { - x = WIDTH - rwidth - 12; - } - } - else { - if ((x - CX) > 0) { - x = x - rwidth - 5; - if (x < 12) { - x = 12; - } - } - else { - x = CX - rwidth / 2; - } - } - var yrect = d["y_v"]; - var ytext = d["y_v"]; - if ((yrect - CY) > 0) { - yrect = yrect - 5 - 20; - ytext = ytext - 5 - 20 + 16; - } - else { - yrect = yrect + 5; - ytext = ytext + 5 + 16; - } - var r = g.append("rect") - .attr('class', 'radartip') - .attr("x", x) - .attr("y", yrect); + g.append("svg:path") + .attr("class", "radarnoteslines") + .attr("d", ligne(notes_circ_valid)); - var txt = g.append("text").text("Note: " + d.note + "/20, moyenne promo: " + d.moy + "/20") - .attr('class', 'radartip') - .attr("x", x + 5) - .attr("y", ytext); - r.attr("width", rwidth).attr("height", 20); - }) - .on("mouseout", function (d) { - d3.selectAll(".radartip").remove() - }); + var ligne_moy = d3.svg + .line() + .x(function (d) { + return d["x_moy"]; + }) + .y(function (d) { + return d["y_moy"]; + }); - /* Valeurs des notes */ - g.selectAll("notes_labels") - .data(notes_valid) - .enter().append("text") - .text(function (d) { return d["note"]; }) - .attr("x", function (d) { - return d["x_v"]; - }) - .attr("y", function (d) { - if (d["y_v"] > CY) - return d["y_v"] + 16; - else - return d["y_v"] - 8; - }) - .attr("class", "note_label"); + g.append("svg:path") + .attr("class", "radarmoylines") + .attr("d", ligne_moy(notes_circ_valid)); - /* Petits points sur les moyennes */ - g.selectAll("circle2") - .data(notes_valid) - .enter().append("circle") - .attr("cx", function (d) { return d["x_moy"]; }) - .attr("cy", function (d) { return d["y_moy"]; }) - .attr("r", function (x, i) { return 2; }) - .style("stroke-width", 0) - .style("stroke", "black") - .style("fill", "rgb(20,90,50)"); + /* Points (notes) */ + g.selectAll("circle1") + .data(notes_valid) + .enter() + .append("circle") + .attr("cx", function (d) { + return d["x_v"]; + }) + .attr("cy", function (d) { + return d["y_v"]; + }) + .attr("r", function (x, i) { + return 3; + }) + .style("stroke-width", 1) + .style("stroke", "black") + .style("fill", "blue") + .on("mouseover", function (d) { + var rwidth = 310; + var x = d["x_v"]; + if (x - CX < 0) { + x = x + 5; + if (x + rwidth + 12 > WIDTH) { + x = WIDTH - rwidth - 12; + } + } else { + if (x - CX > 0) { + x = x - rwidth - 5; + if (x < 12) { + x = 12; + } + } else { + x = CX - rwidth / 2; + } + } + var yrect = d["y_v"]; + var ytext = d["y_v"]; + if (yrect - CY > 0) { + yrect = yrect - 5 - 20; + ytext = ytext - 5 - 20 + 16; + } else { + yrect = yrect + 5; + ytext = ytext + 5 + 16; + } + var r = g + .append("rect") + .attr("class", "radartip") + .attr("x", x) + .attr("y", yrect); - /* Valeurs sur axe */ - g.selectAll("textaxis") - .data(R_AXIS_TICS) - .enter().append("text") - .text(String) - .attr("x", CX - 10) - .attr("y", function (x, i) { return CY - x * RR / 20 + 6; }) - .attr("class", "textaxis"); + var txt = g + .append("text") + .text("Note: " + d.note + "/20, moyenne promo: " + d.moy + "/20") + .attr("class", "radartip") + .attr("x", x + 5) + .attr("y", ytext); + r.attr("width", rwidth).attr("height", 20); + }) + .on("mouseout", function (d) { + d3.selectAll(".radartip").remove(); + }); - /* Noms des modules */ - g.selectAll("text_modules") - .data(notes) - .enter().append("text") - .text(function (d) { return d['code']; }) - .attr("x", function (d) { return d['x_label']; }) - .attr("y", function (d) { return d['y_label']; }) - .attr("dx", 0) - .attr("dy", 0) - .on("mouseover", function (d) { - var x = d["x_label"]; - var yrect = d["y_label"]; - var ytext = d["y_label"]; - var titre = d['titre'].replace("'", "'").substring(0, 64); - var rwidth = titre.length * 9; // rough estimate of string width in pixels - if ((x - CX) < 0) { - x = x + 5; - if (x + rwidth + 12 > WIDTH) { - x = WIDTH - rwidth - 12; - } - } - else { - if ((x - CX) > 0) { - x = x - rwidth - 5; - if (x < 12) { - x = 12; - } - } - else { - x = CX - rwidth / 2; - } - } - if ((yrect - CY) > 0) { - yrect = yrect - 5 - 20; - ytext = ytext - 5 - 20 + 16; - } - else { - yrect = yrect + 5; - ytext = ytext + 5 + 16; - } - var r = g.append("rect") - .attr('class', 'radartip') - .attr("x", x) - .attr("y", yrect) - .attr("height", 20) - .attr("width", rwidth); - var txt = g.append("text").text(titre) - .attr('class', 'radartip') - .attr("x", x + 5) - .attr("y", ytext); - }) - .on("mouseout", function (d) { - d3.selectAll(".radartip").remove() - }); + /* Valeurs des notes */ + g.selectAll("notes_labels") + .data(notes_valid) + .enter() + .append("text") + .text(function (d) { + return d["note"]; + }) + .attr("x", function (d) { + return d["x_v"]; + }) + .attr("y", function (d) { + if (d["y_v"] > CY) return d["y_v"] + 16; + else return d["y_v"] - 8; + }) + .attr("class", "note_label"); + + /* Petits points sur les moyennes */ + g.selectAll("circle2") + .data(notes_valid) + .enter() + .append("circle") + .attr("cx", function (d) { + return d["x_moy"]; + }) + .attr("cy", function (d) { + return d["y_moy"]; + }) + .attr("r", function (x, i) { + return 2; + }) + .style("stroke-width", 0) + .style("stroke", "black") + .style("fill", "rgb(20,90,50)"); + + /* Valeurs sur axe */ + g.selectAll("textaxis") + .data(R_AXIS_TICS) + .enter() + .append("text") + .text(String) + .attr("x", CX - 10) + .attr("y", function (x, i) { + return CY - (x * RR) / 20 + 6; + }) + .attr("class", "textaxis"); + + /* Noms des modules */ + g.selectAll("text_modules") + .data(notes) + .enter() + .append("text") + .text(function (d) { + return d["code"]; + }) + .attr("x", function (d) { + return d["x_label"]; + }) + .attr("y", function (d) { + return d["y_label"]; + }) + .attr("dx", 0) + .attr("dy", 0) + .on("mouseover", function (d) { + var x = d["x_label"]; + var yrect = d["y_label"]; + var ytext = d["y_label"]; + var titre = d["titre"].replace("'", "'").substring(0, 64); + var rwidth = titre.length * 9; // rough estimate of string width in pixels + if (x - CX < 0) { + x = x + 5; + if (x + rwidth + 12 > WIDTH) { + x = WIDTH - rwidth - 12; + } + } else { + if (x - CX > 0) { + x = x - rwidth - 5; + if (x < 12) { + x = 12; + } + } else { + x = CX - rwidth / 2; + } + } + if (yrect - CY > 0) { + yrect = yrect - 5 - 20; + ytext = ytext - 5 - 20 + 16; + } else { + yrect = yrect + 5; + ytext = ytext + 5 + 16; + } + var r = g + .append("rect") + .attr("class", "radartip") + .attr("x", x) + .attr("y", yrect) + .attr("height", 20) + .attr("width", rwidth); + var txt = g + .append("text") + .text(titre) + .attr("class", "radartip") + .attr("x", x + 5) + .attr("y", ytext); + }) + .on("mouseout", function (d) { + d3.selectAll(".radartip").remove(); + }); } diff --git a/app/tables/visu_assiduites.py b/app/tables/visu_assiduites.py index 9d1adff3..82ba0368 100644 --- a/app/tables/visu_assiduites.py +++ b/app/tables/visu_assiduites.py @@ -114,9 +114,18 @@ class RowAssi(tb.Row): compte_justificatifs = scass.filter_by_date( etud.justificatifs, Justificatif, self.dates[0], self.dates[1] - ).count() + ) - self.add_cell("justificatifs", "Justificatifs", f"{compte_justificatifs}") + compte_justificatifs_att = compte_justificatifs.filter(Justificatif.etat == 2) + + self.add_cell( + "justificatifs_att", + "Justificatifs en Attente", + f"{compte_justificatifs_att.count()}", + ) + self.add_cell( + "justificatifs", "Justificatifs", f"{compte_justificatifs.count()}" + ) def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]: retour: dict[str, tuple[str, float, float]] = { diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index 4cbc8f59..28c4b615 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -1,14 +1,14 @@ {% include "assiduites/widgets/toast.j2" %} {% block pageContent %}
        -

        Justifier des assiduités

        +

        Justifier des absences ou retards

        {% include "assiduites/widgets/tableau_base.j2" %}
        - + {% include "assiduites/widgets/tableau_justi.j2" %}
        -
        +
        @@ -19,8 +19,9 @@
        Date de début + Journée(s) entière(s)
        -
        +
        Date de fin
        @@ -110,16 +111,15 @@ function validateFields() { const field = document.querySelector('.justi-form') - const in_date_debut = field.querySelector('#justi_date_debut'); - const in_date_fin = field.querySelector('#justi_date_fin'); + const { deb, fin } = getDates() - if (in_date_debut.value == "" || in_date_fin.value == "") { - openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin."), "", color = "crimson"); + if (deb == "" || fin == "") { + openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson"); return false; } - const date_debut = moment.tz(in_date_debut.value, TIMEZONE); - const date_fin = moment.tz(in_date_fin.value, TIMEZONE); + const date_debut = moment.tz(deb, TIMEZONE); + const date_fin = moment.tz(fin, TIMEZONE); if (date_fin.isBefore(date_debut)) { openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson"); @@ -130,16 +130,16 @@ } function fieldsToJustificatif() { - const field = document.querySelector('.justi-form') + const field = document.querySelector('.justi-form.page') + + const { deb, fin } = getDates() - const date_debut = field.querySelector('#justi_date_debut').value; - const date_fin = field.querySelector('#justi_date_fin').value; const etat = field.querySelector('#justi_etat').value; const raison = field.querySelector('#justi_raison').value; return { - date_debut: date_debut, - date_fin: date_fin, + date_debut: moment.tz(deb, TIMEZONE).format(), + date_fin: moment.tz(fin, TIMEZONE).format(), etat: etat, raison: raison, } @@ -218,11 +218,46 @@ } + function dayOnly() { + if (document.getElementById('justi_journee').checked) { + document.getElementById("justi_date_debut").type = "date" + document.getElementById("justi_date_fin").type = "date" + } else { + document.getElementById("justi_date_debut").type = "datetime-local" + document.getElementById("justi_date_fin").type = "datetime-local" + } + } + + function getDates() { + if (document.querySelector('.page #justi_journee').checked) { + const date_str_deb = document.querySelector(".page #justi_date_debut").value + const date_str_fin = document.querySelector(".page #justi_date_debut").value + + + + return { + "deb": date_str_deb ? `${date_str_deb}T${assi_morning}` : "", + "fin": date_str_fin ? `${date_str_fin}T${assi_evening}` : "", + } + } + + return { + "deb": document.querySelector(".page #justi_date_debut").value, + "fin": document.querySelector(".page #justi_date_fin").value, + } + } const etudid = {{ sco.etud.id }}; + + const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false; + const assi_morning = '{{assi_morning}}'; + const assi_evening = '{{assi_evening}}'; + window.onload = () => { loadAll(); + document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() }); + dayOnly() } {% endblock pageContent %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/bilan_dept.j2 b/app/templates/assiduites/pages/bilan_dept.j2 index aea0bb8b..5da0ae64 100644 --- a/app/templates/assiduites/pages/bilan_dept.j2 +++ b/app/templates/assiduites/pages/bilan_dept.j2 @@ -6,6 +6,10 @@

        Justificatifs en attente (ou modifiés)

        + + + + {% include "assiduites/widgets/tableau_justi.j2" %}
        @@ -29,18 +33,21 @@
        {% endblock pageContent %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/liste_assiduites.j2 b/app/templates/assiduites/pages/liste_assiduites.j2 index d1802180..323a2db6 100644 --- a/app/templates/assiduites/pages/liste_assiduites.j2 +++ b/app/templates/assiduites/pages/liste_assiduites.j2 @@ -3,11 +3,17 @@

        Liste de l'assiduité et des justificatifs de {{sco.etud.nomprenom}}

        {% include "assiduites/widgets/tableau_base.j2" %} -

        Assiduités :

        - +

        Assiduité :

        + + + + {% include "assiduites/widgets/tableau_assi.j2" %}

        Justificatifs :

        - + + + + {% include "assiduites/widgets/tableau_justi.j2" %}
        • Detail
        • @@ -27,20 +33,20 @@
        • Supprimer : Permet de supprimer le justificatif (Action Irréversible)
        -

        Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonoir sous le titre du tableau.

        +

        Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonnoir sous le titre du tableau.

        -

        Gestion des Assiduités

        +

        Gestion de l'assiduité

        Faites clic droit sur une ligne du tableau pour afficher le menu contextuel :

          -
        • Détails : Affiche les détails de l'assiduité sélectionnée
        • -
        • Editer : Permet de modifier l'assiduité (moduleimpl, etat)
        • -
        • Supprimer : Permet de supprimer l'assiduité (Action Irréversible)
        • +
        • Détails : affiche les détails de l'assiduité sélectionnée
        • +
        • Éditer : modifier l'élément (module, état)
        • +
        • Supprimer : supprimer l'élément (action irréversible)
        -

        Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonoir sous le titre du tableau.

        +

        Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonnoir sous le titre du tableau.

        @@ -48,8 +54,43 @@ \ No newline at end of file diff --git a/app/templates/assiduites/pages/liste_semestre.j2 b/app/templates/assiduites/pages/liste_semestre.j2 new file mode 100644 index 00000000..90c996da --- /dev/null +++ b/app/templates/assiduites/pages/liste_semestre.j2 @@ -0,0 +1,94 @@ +{% block pageContent %} +
        +

        Assiduites et justificatifs de {{sem}}

        + {% include "assiduites/widgets/tableau_base.j2" %} + +

        Assiduité :

        + + + + + {% include "assiduites/widgets/tableau_assi.j2" %} +

        Justificatifs :

        + + + + + {% include "assiduites/widgets/tableau_justi.j2" %} + +
        + +{% endblock pageContent %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/signal_assiduites_diff.j2 b/app/templates/assiduites/pages/signal_assiduites_diff.j2 index 700b84f7..5159c702 100644 --- a/app/templates/assiduites/pages/signal_assiduites_diff.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_diff.j2 @@ -1,16 +1,16 @@ -

        Signalement différé des assiduités {{gr |safe}}

        -
        +

        Signalement différé de l'assiduité {{gr |safe}}

        +

        Explication de la saisie différée

        -

        Si la colonne n'est pas valide elle sera affichée en rouge, passez votre curseur sur la colonne pour afficher +

        Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher le message d'erreur

        Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance (préférence de département)

        -

        Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur - moduleimpl.

        -

        Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants

        -

        Le dernier des boutons retire l'assiduité.

        +

        Modifier le module alors que des informations d'assiduité sont déjà enregistrées pour la période changera leur + module.

        +

        Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants

        +

        Le dernier des boutons retire l'information présente.

        Vous pouvez ajouter des colonnes en appuyant sur le bouton +

        -

        Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne +

        Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.

        {{sem | safe }}

        diff --git a/app/templates/assiduites/pages/signal_assiduites_etud.j2 b/app/templates/assiduites/pages/signal_assiduites_etud.j2 index 2222cbe4..d0945cff 100644 --- a/app/templates/assiduites/pages/signal_assiduites_etud.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_etud.j2 @@ -32,6 +32,18 @@

        + + {% if saisie_eval %} +
        +
        +

        + La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation.
        + Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation +

        + retourner sur la page de l'évaluation +
        + {% endif %} + {{diff | safe}}
        @@ -62,16 +74,16 @@

        Vous pouvez justifier rapidement une assiduité en saisisant l'assiduité puis en appuyant sur "Justifier"

        Explication de la saisie différée

        -

        Si la colonne n'est pas valide elle sera affichée en rouge, passez votre curseur sur la colonne pour afficher +

        Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher le message d'erreur

        Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance (préférence de département)

        -

        Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur - moduleimpl.

        -

        Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants

        -

        Le dernier des boutons retire l'assiduité.

        +

        Modifier le module alors que des informations sont déjà enregistrées pour la période changera leur + module.

        +

        Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants

        +

        Le dernier des boutons retire l'information présente.

        Vous pouvez ajouter des colonnes en appuyant sur le bouton +

        -

        Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne +

        Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.

        @@ -118,7 +130,20 @@ window.forceModule = "{{ forcer_module }}" window.forceModule = window.forceModule == "True" ? true : false + const date_deb = "{{date_deb}}"; + const date_fin = "{{date_fin}}"; + + {% if saisie_eval %} + createColumn( + date_deb, + date_fin, + {{ moduleimpl_id }} + ); + window.location.href = "#saisie_eval" + getAndUpdateCol(1) + {% else %} createColumn(); + {% endif %} diff --git a/app/templates/assiduites/pages/visu_assi.j2 b/app/templates/assiduites/pages/visu_assi.j2 index 07fb6974..f947928f 100644 --- a/app/templates/assiduites/pages/visu_assi.j2 +++ b/app/templates/assiduites/pages/visu_assi.j2 @@ -16,13 +16,13 @@ value="{{date_fin}}"> - {{scu.ICON_XLS|safe}} + {{scu.ICON_XLS|safe}} {{tableau | safe}}
        -Les comptes sont exprimés en {{ assi_metric }}. +Les comptes sont exprimés en {{ assi_metric | lower}}s.
        \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 5a795af1..c623ccce 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -18,6 +18,9 @@ document.addEventListener("click", () => { contextMenu.style.display = "none"; + if (contextMenu.childElementCount > 3) { + contextMenu.removeChild(contextMenu.lastElementChild) + } }); editOption.addEventListener("click", () => { @@ -57,8 +60,6 @@ deleteJustificatif(obj_id); } loadAll(); - - } }); @@ -94,6 +95,22 @@ } } + 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; }) @@ -150,7 +167,7 @@ paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => { if (currentPageAssiduites > 1) { currentPageAssiduites--; - paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + "" assiduiteCallBack(array); } @@ -159,7 +176,7 @@ paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => { if (currentPageAssiduites < totalPages) { currentPageAssiduites++; - paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + "" assiduiteCallBack(array); } }) @@ -199,8 +216,12 @@ 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); @@ -230,8 +251,8 @@ } function loadAll() { - try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { } - try { getAllJustificatifsFromEtud(etudid, justificatifCallBack) } catch (_) { } + try { getAssi(assiduiteCallBack) } catch { } + try { getJusti(justificatifCallBack) } catch { } } function order(keyword, callback = () => { }, el, assi = true) { @@ -249,6 +270,13 @@ 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; @@ -266,351 +294,14 @@ if (assi) { orderAssiduites = !orderAssiduites; - getAllAssiduitesFromEtud(etudid, (a) => { call(a, orderAssiduites) }) + getAssi((a) => { call(a, orderAssiduites) }); } else { orderJustificatifs = !orderJustificatifs; - getAllJustificatifsFromEtud(etudid, (a) => { call(a, orderJustificatifs) }) + getJusti((a) => { call(a, orderJustificatifs) }); } } - function filter(assi = true) { - if (assi) { - let html = ` -
        -

        Affichage des colonnes:

        -
        - - - - - - -
        -
        -

        Filtrage des colonnes:

        - - Date de saisie - - - - - Date de début - - - - - Date de fin - - - - - Etat - - - - - - Module - - - - Est Justifiée - - -
        - `; - const span = document.createElement('span'); - span.innerHTML = html - html = span.firstElementChild - - const filterHead = html.querySelector('.filter-head'); - filterHead.innerHTML = "" - let cols = ["entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"]; - - cols.forEach((k) => { - const label = document.createElement('label') - label.classList.add('f-label') - const s = document.createElement('span'); - s.textContent = columnTranslator(k); - - - const input = document.createElement('input'); - input.classList.add('chk') - input.type = "checkbox" - input.name = k - input.id = k; - input.checked = filterAssiduites.columns.includes(k) - - label.appendChild(s) - label.appendChild(input) - filterHead.appendChild(label) - }) - - const sl = html.querySelector('.filter-line #moduleimpl_id'); - let opts = [] - Object.keys(moduleimpls).forEach((k) => { - const opt = document.createElement('option'); - opt.value = k == null ? "null" : k; - opt.textContent = moduleimpls[k]; - opts.push(opt); - }) - - opts = opts.sort((a, b) => { - return a.value < b.value - }) - - sl.append(...opts); - - // Mise à jour des filtres - - Object.keys(filterAssiduites.filters).forEach((key) => { - const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; - if (key.indexOf('date') != -1) { - l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref; - l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm"); - - } else if (key.indexOf('etat') != -1) { - l.querySelectorAll('input').forEach((e) => { - e.checked = filterAssiduites.filters[key].includes(e.value) - }) - } else if (key.indexOf("module") != -1) { - l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key]; - } else if (key.indexOf("est_just") != -1) { - l.querySelector('#est_just').value = filterAssiduites.filters[key]; - } - }) - - openPromptModal("Filtrage des assiduités", html, () => { - - const columns = [...document.querySelectorAll('.chk')] - .map((el) => { if (el.checked) return el.id }) - .filter((el) => el) - - filterAssiduites.columns = columns - filterAssiduites.filters = {} - //reste des filtres - - const lines = [...document.querySelectorAll('.filter-line')]; - - lines.forEach((l) => { - const key = l.querySelector('.filter-title').getAttribute('for'); - - if (key.indexOf('date') != -1) { - const pref = l.querySelector(`#${key}_pref`).value; - const time = l.querySelector(`#${key}_time`).value; - if (l.querySelector(`#${key}_time`).value != "") { - filterAssiduites.filters[key] = { - pref: pref, - time: new moment.tz(time, TIMEZONE) - } - } - } else if (key.indexOf('etat') != -1) { - filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); - } else if (key.indexOf("module") != -1) { - filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value; - } else if (key.indexOf("est_just") != -1) { - filterAssiduites.filters[key] = l.querySelector('#est_just').value; - } - }) - - - getAllAssiduitesFromEtud(etudid, assiduiteCallBack) - - }, () => { }, "#7059FF"); - } else { - let html = ` -
        -

        Affichage des colonnes:

        -
        - - - - - - -
        -
        -

        Filtrage des colonnes:

        - - Date de saisie - - - - - Date de début - - - - - Date de fin - - - - - Etat - - - - - -
        - `; - const span = document.createElement('span'); - span.innerHTML = html - html = span.firstElementChild - - const filterHead = html.querySelector('.filter-head'); - filterHead.innerHTML = "" - let cols = ["entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"]; - - 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) - }) - } - }) - - 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 moment.tz(time, TIMEZONE) - } - } - } else if (key.indexOf('etat') != -1) { - filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); - } - }) - - - getAllJustificatifsFromEtud(etudid, justificatifCallBack) - - }, () => { }, "#7059FF"); - } - } function columnTranslator(colName) { switch (colName) { @@ -632,6 +323,8 @@ return "Fichier"; case "etudid": return "Etudiant"; + case "formsemestre": + return "Semestre"; } } @@ -641,6 +334,103 @@ 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" + + li.addEventListener('click', () => { + let obj_id = selectedRow.getAttribute('obj_id'); + assiduite = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id }) + if (assiduite && !assiduite[0].est_just && assiduite[0].etat != "PRESENT") { + fastJustify(assiduite[0]) + } else { + openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée ou ne peut pas l'être.")) + } + }) + + 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") + + }, () => { }, "green"); + } + + 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]; + } diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index c4b67773..c136a56e 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -57,17 +57,6 @@ renderPaginationButtons(justi, false); } - - function getEtudiant(id) { - if (id in etuds) { - return etuds[id]; - } - getSingleEtud(id); - - return etuds[id]; - - } - function renderTableJustificatifs(page, justificatifs) { generateTableHead(filterJustificatifs.columns, false) @@ -99,7 +88,13 @@ } else if (k.indexOf('etudid') != -1) { const e = getEtudiant(justificatif.etudid); - td.textContent = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`; + td.innerHTML = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`; + } 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) { @@ -169,7 +164,7 @@ ${etat}
        - Créer par + Créé par ${user}
        @@ -250,7 +245,7 @@ // 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.log(err); + console.error(err); }); } @@ -419,7 +414,6 @@ return el.parentElement.querySelector('a').textContent; }); - console.log(justif_id, files); sync_post( path, { @@ -458,7 +452,207 @@ }) } + function filterJusti(dept = false) { + let dept_html_head = ` + + + ` + + let dept_html_body = ` + + Recherche dans les semestre + + + ` + + let html = ` +
        +

        Affichage des colonnes:

        +
        + ${dept ? dept_html_head : ""} + + + + + + +
        +
        +

        Filtrage des colonnes:

        + + Date de saisie + + + + + Date de début + + + + + Date de fin + + + + + Etat + + + + + + + Rechercher dans les étudiants + + + ${dept ? dept_html_body : ""} +
        + `; + 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 moment.tz(time, TIMEZONE) + } + } + } 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(); + + + }, () => { }, "#7059FF"); + } + + function downloadJusti() { + getJusti((d) => { toCSV(d, filterJustificatifs) }) + } + + function getJusti(action) { + try { getAllJustificatifsFromEtud(etudid, action, true, assi_limit_annee) } catch (_) { } + }