diff --git a/app/models/events.py b/app/models/events.py index 9725f3c8a..b94549e76 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -36,18 +36,21 @@ class Scolog(db.Model): class ScolarNews(db.Model): """Nouvelles pour page d'accueil""" - NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id) - NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id) - NEWS_FORM = "FORM" # modification formation (object=formation_id) - NEWS_SEM = "SEM" # creation semestre (object=None) NEWS_ABS = "ABS" # saisie absence + NEWS_APO = "APO" # changements de codes APO + NEWS_FORM = "FORM" # modification formation (object=formation_id) + NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id) NEWS_MISC = "MISC" # unused + NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id) + NEWS_SEM = "SEM" # creation semestre (object=None) NEWS_MAP = { - NEWS_INSCR: "inscription d'étudiants", - NEWS_NOTE: "saisie note", + NEWS_ABS: "saisie absence", + NEWS_APO: "modif. code Apogée", NEWS_FORM: "modification formation", - NEWS_SEM: "création semestre", + NEWS_INSCR: "inscription d'étudiants", NEWS_MISC: "opération", # unused + NEWS_NOTE: "saisie note", + NEWS_SEM: "création semestre", } NEWS_TYPES = list(NEWS_MAP.keys()) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 0d126fc7f..fda723839 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -375,7 +375,7 @@ class FormSemestre(db.Model): return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" def sem_modalite(self) -> str: - """Le semestre et la modialité, ex "S2 FI" ou "S3 APP" """ + """Le semestre et la modalité, ex "S2 FI" ou "S3 APP" """ if self.semestre_id > 0: descr_sem = f"S{self.semestre_id}" else: diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index 0943649cf..48795edfc 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -486,7 +486,10 @@ class JuryPE(object): sesdates = [ pe_tagtable.conversionDate_StrToDate(sem["date_fin"]) for sem in sessems ] # association 1 date -> 1 semestrePE pour les semestres de l'étudiant - lastdate = max(sesdates) # date de fin de l'inscription la plus récente + if sesdates: + lastdate = max(sesdates) # date de fin de l'inscription la plus récente + else: + return False # if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print(" derniere inscription = ", lastDateSem) @@ -585,7 +588,7 @@ class JuryPE(object): for (i, fid) in enumerate(lesFids): if pe_tools.PE_DEBUG: pe_tools.pe_print( - u"%d) Semestre taggué %s (avec classement dans groupe)" + "%d) Semestre taggué %s (avec classement dans groupe)" % (i + 1, fid) ) self.add_semtags_in_jury(fid) @@ -620,7 +623,7 @@ class JuryPE(object): nbinscrit = self.semTagDict[fid].get_nbinscrits() if pe_tools.PE_DEBUG: pe_tools.pe_print( - u" - %d étudiants classés " % (nbinscrit) + " - %d étudiants classés " % (nbinscrit) + ": " + ",".join( [etudid for etudid in self.semTagDict[fid].get_etudids()] @@ -628,12 +631,12 @@ class JuryPE(object): ) if lesEtudidsManquants: pe_tools.pe_print( - u" - dont %d étudiants manquants ajoutés aux données du jury" + " - dont %d étudiants manquants ajoutés aux données du jury" % (len(lesEtudidsManquants)) + ": " + ", ".join(lesEtudidsManquants) ) - pe_tools.pe_print(u" - Export csv") + pe_tools.pe_print(" - Export csv") filename = self.NOM_EXPORT_ZIP + self.semTagDict[fid].nom + ".csv" self.zipfile.writestr(filename, self.semTagDict[fid].str_tagtable()) @@ -742,7 +745,7 @@ class JuryPE(object): for fid in fids_finaux: if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: - pe_tools.pe_print(u" - semestre final %s" % (fid)) + pe_tools.pe_print(" - semestre final %s" % (fid)) settag = pe_settag.SetTag( nom, parcours=parcours ) # Le set tag fusionnant les données @@ -762,7 +765,7 @@ class JuryPE(object): for ffid in settag.get_Fids_in_settag(): if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 1: pe_tools.pe_print( - u" -> ajout du semestre tagué %s" % (ffid) + " -> ajout du semestre tagué %s" % (ffid) ) self.add_semtags_in_jury(ffid) settag.set_SemTagDict( @@ -791,7 +794,7 @@ class JuryPE(object): if nbreEtudInscrits > 0: if pe_tools.PE_DEBUG: pe_tools.pe_print( - u"%d) %s avec interclassement sur la promo" % (i + 1, nom) + "%d) %s avec interclassement sur la promo" % (i + 1, nom) ) if nom in ["S1", "S2", "S3", "S4"]: settag.set_SetTagDict(self.semTagDict) @@ -802,7 +805,7 @@ class JuryPE(object): else: if pe_tools.PE_DEBUG: pe_tools.pe_print( - u"%d) Pas d'interclassement %s sur la promo faute de notes" + "%d) Pas d'interclassement %s sur la promo faute de notes" % (i + 1, nom) ) @@ -1152,11 +1155,14 @@ class JuryPE(object): return sesSems # ********************************************** - def calcul_anneePromoDUT_d_un_etudiant(self, etudid): + def calcul_anneePromoDUT_d_un_etudiant(self, etudid) -> int: """Calcule et renvoie la date de diplome prévue pour un étudiant fourni avec son etudid - en fonction de sesSemestres de scolarisation""" - sesSemestres = self.get_semestresDUT_d_un_etudiant(etudid) - return max([get_annee_diplome_semestre(sem) for sem in sesSemestres]) + en fonction de ses semestres de scolarisation""" + semestres = self.get_semestresDUT_d_un_etudiant(etudid) + if semestres: + return max([get_annee_diplome_semestre(sem) for sem in semestres]) + else: + return None # ********************************************* # Fonctions d'affichage pour debug @@ -1184,18 +1190,21 @@ class JuryPE(object): chaine += "\n" return chaine - def get_date_entree_etudiant(self, etudid): - """Renvoie la date d'entree d'un étudiant""" - return str( - min([int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"]]) - ) + def get_date_entree_etudiant(self, etudid) -> str: + """Renvoie la date d'entree d'un étudiant: "1996" """ + annees_debut = [ + int(sem["annee_debut"]) for sem in self.ETUDINFO_DICT[etudid]["sems"] + ] + if annees_debut: + return str(min(annees_debut)) + return "" # ---------------------------------------------------------------------------------------- # Fonctions # ---------------------------------------------------------------------------------------- -def get_annee_diplome_semestre(sem): +def get_annee_diplome_semestre(sem) -> int: """Pour un semestre donne, décrit par le biais du dictionnaire sem usuel : sem = {'formestre_id': ..., 'semestre_id': ..., 'annee_debut': ...}, à condition qu'il soit un semestre de formation DUT, diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 01b94c064..1c708ca5c 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -385,12 +385,16 @@ class GenTable(object): colspan_count = colspan else: colspan_txt = "" + attrs = row.get("_%s_td_attrs" % cid, "") + order = row.get(f"_{cid}_order") + if order: + attrs += f' data-order="{order}"' r.append( "<%s%s %s%s%s>%s" % ( elem, std, - row.get("_%s_td_attrs" % cid, ""), + attrs, klass, colspan_txt, content, diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index c6811d454..fd25e5da3 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -43,10 +43,8 @@ import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType from app.scodoc.TrivialFormulator import TrivialFormulator -from app.scodoc.gen_tables import GenTable from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( - ScoGenError, ScoValueError, ScoLockedFormError, ScoNonEmptyFormationObject, @@ -61,7 +59,6 @@ from app.scodoc import sco_edit_module from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_preferences from app.scodoc import sco_tag_module _ueEditor = ndb.EditableTable( @@ -1355,93 +1352,6 @@ def ue_is_locked(ue_id): return len(r) > 0 -# ---- Table recap formation -def formation_table_recap(formation_id, format="html"): - """Table recapitulant formation.""" - from app.scodoc import sco_formations - - F = sco_formations.formation_list(args={"formation_id": formation_id}) - if not F: - raise ScoValueError("invalid formation_id") - F = F[0] - T = [] - ues = ue_list(args={"formation_id": formation_id}) - for ue in ues: - Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]}) - for Mat in Matlist: - Modlist = sco_edit_module.module_list( - args={"matiere_id": Mat["matiere_id"]} - ) - for Mod in Modlist: - Mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls( - Mod["module_id"] - ) - # - T.append( - { - "UE_acro": ue["acronyme"], - "Mat_tit": Mat["titre"], - "Mod_tit": Mod["abbrev"] or Mod["titre"], - "Mod_code": Mod["code"], - "Mod_coef": Mod["coefficient"], - "Mod_sem": Mod["semestre_id"], - "nb_moduleimpls": Mod["nb_moduleimpls"], - "heures_cours": Mod["heures_cours"], - "heures_td": Mod["heures_td"], - "heures_tp": Mod["heures_tp"], - "ects": Mod["ects"], - } - ) - columns_ids = [ - "UE_acro", - "Mat_tit", - "Mod_tit", - "Mod_code", - "Mod_coef", - "Mod_sem", - "nb_moduleimpls", - "heures_cours", - "heures_td", - "heures_tp", - "ects", - ] - titles = { - "UE_acro": "UE", - "Mat_tit": "Matière", - "Mod_tit": "Module", - "Mod_code": "Code", - "Mod_coef": "Coef.", - "Mod_sem": "Sem.", - "nb_moduleimpls": "Nb utilisé", - "heures_cours": "Cours (h)", - "heures_td": "TD (h)", - "heures_tp": "TP (h)", - "ects": "ECTS", - } - - title = ( - """Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s""" - % F - ) - tab = GenTable( - columns_ids=columns_ids, - rows=T, - titles=titles, - origin="Généré par %s le " % scu.sco_version.SCONAME - + scu.timedate_human_repr() - + "", - caption=title, - html_caption=title, - html_class="table_leftalign", - base_url="%s?formation_id=%s" % (request.base_url, formation_id), - page_title=title, - html_title="

" + title + "

", - pdf_title=title, - preferences=sco_preferences.SemPreferences(), - ) - return tab.make_page(format=format) - - def ue_list_semestre_ids(ue: dict): """Liste triée des numeros de semestres des modules dans cette UE Il est recommandable que tous les modules d'une UE aient le même indice de semestre. diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py new file mode 100644 index 000000000..16aa336d2 --- /dev/null +++ b/app/scodoc/sco_formation_recap.py @@ -0,0 +1,192 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Table recap formation (avec champs éditables) +""" +import io +from zipfile import ZipFile, BadZipfile + +from flask import send_file, url_for +from flask import g, request +from flask_login import current_user + +from app.models import Formation, FormSemestre, UniteEns, Module +from app.models.formations import Matiere + +from app.scodoc.gen_tables import GenTable +from app.scodoc.sco_permissions import Permission +from app.scodoc import sco_preferences +import app.scodoc.sco_utils as scu + + +# ---- Table recap formation +def formation_table_recap(formation_id, format="html"): + """Table recapitulant formation.""" + T = [] + formation = Formation.query.get_or_404(formation_id) + ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero) + can_edit = current_user.has_permission(Permission.ScoChangeFormation) + li = 0 + for ue in ues: + # L'UE + T.append( + { + "sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-", + "_sem_order": f"{li:04d}", + "code": ue.acronyme, + "titre": ue.titre or "", + "_titre_target": url_for( + "notes.ue_edit", + scodoc_dept=g.scodoc_dept, + ue_id=ue.id, + ) + if can_edit + else None, + "apo": ue.code_apogee or "", + "_apo_td_attrs": f""" data-oid="{ue.id}" data-value="{ue.code_apogee or ''}" """, + "coef": ue.coefficient or "", + "ects": ue.ects, + "_css_row_class": f"ue ue_", + } + ) + li += 1 + matieres = ue.matieres.order_by(Matiere.numero) + for mat in matieres: + modules = mat.modules.order_by(Module.numero) + for mod in modules: + nb_moduleimpls = mod.modimpls.count() + # le module (ou ressource ou sae) + T.append( + { + "sem": f"S{mod.semestre_id}" + if mod.semestre_id is not None + else "-", + "_sem_order": f"{li:04d}", + "code": mod.code, + "titre": mod.abbrev or mod.titre, + "_titre_target": url_for( + "notes.module_edit", + scodoc_dept=g.scodoc_dept, + module_id=mod.id, + ) + if can_edit + else None, + "apo": mod.code_apogee, + "_apo_td_attrs": f""" data-oid="{mod.id}" data-value="{mod.code_apogee or ''}" """, + "coef": mod.coefficient, + "nb_moduleimpls": nb_moduleimpls, + "heures_cours": mod.heures_cours, + "heures_td": mod.heures_td, + "heures_tp": mod.heures_tp, + "_css_row_class": f"mod {mod.type_abbrv()}", + } + ) + columns_ids = [ + "sem", + "code", + "apo", + # "mat", inutile d'afficher la matière + "titre", + "coef", + "ects", + "nb_moduleimpls", + "heures_cours", + "heures_td", + "heures_tp", + ] + titles = { + "ue": "UE", + "mat": "Matière", + "titre": "Titre", + "code": "Code", + "apo": "Apo", + "coef": "Coef.", + "sem": "Sem.", + "nb_moduleimpls": "Nb utilisé", + "heures_cours": "Cours (h)", + "heures_td": "TD (h)", + "heures_tp": "TP (h)", + "ects": "ECTS", + } + + title = f"""Formation {formation.titre} ({formation.acronyme}) + [version {formation.version}] code {formation.formation_code}""" + html_class = "stripe cell-border compact hover order-column formation_table_recap" + if current_user.has_permission(Permission.ScoEditApo): + html_class += " apo_editable" + + tab = GenTable( + columns_ids=columns_ids, + rows=T, + titles=titles, + origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}", + caption=title, + html_caption=title, + html_class=html_class, + html_class_ignore_default=True, + html_table_attrs=f""" + data-apo_ue_save_url="{url_for('notes.ue_set_apo', scodoc_dept=g.scodoc_dept)}" + data-apo_mod_save_url="{url_for('notes.module_set_apo', scodoc_dept=g.scodoc_dept)}" + """, + html_with_td_classes=True, + base_url=f"{request.base_url}?formation_id={formation_id}", + page_title=title, + html_title=f"

{title}

", + pdf_title=title, + preferences=sco_preferences.SemPreferences(), + table_id="formation_table_recap", + ) + return tab.make_page(format=format, javascripts=["js/formation_recap.js"]) + + +def export_recap_formations_annee_scolaire(annee_scolaire): + """Exporte un zip des recap (excel) des formatons de tous les semestres + de l'année scolaire indiquée. + """ + annee_scolaire = int(annee_scolaire) + data = io.BytesIO() + zip_file = ZipFile(data, "w") + formsemestres = FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id).filter( + FormSemestre.date_debut >= scu.date_debut_anne_scolaire(annee_scolaire), + FormSemestre.date_debut <= scu.date_fin_anne_scolaire(annee_scolaire), + ) + formation_ids = {formsemestre.formation.id for formsemestre in formsemestres} + for formation_id in formation_ids: + formation = Formation.query.get(formation_id) + xls = formation_table_recap(formation_id, format="xlsx").data + filename = ( + scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX + ) + zip_file.writestr(filename, xls) + zip_file.close() + data.seek(0) + return send_file( + data, + mimetype="application/zip", + download_name=f"formations-{g.scodoc_dept}-{annee_scolaire}-{annee_scolaire+1}.zip", + as_attachment=True, + ) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index d47892019..ff91c1495 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -871,6 +871,20 @@ def annee_scolaire_debut(year, month): return int(year) - 1 +def date_debut_anne_scolaire(annee_scolaire: int) -> datetime: + """La date de début de l'année scolaire + = 1er aout + """ + return datetime.datetime(year=annee_scolaire, month=8, day=1) + + +def date_fin_anne_scolaire(annee_scolaire: int) -> datetime: + """La date de fin de l'année scolaire + = 31 juillet de l'année suivante + """ + return datetime.datetime(year=annee_scolaire + 1, month=7, day=31) + + def sem_decale_str(sem): """'D' si semestre decalé, ou ''""" # considère "décalé" les semestre impairs commençant entre janvier et juin diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 24548b28c..6a0d9e73d 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1056,7 +1056,7 @@ span.wtf-field ul.errors li { } .configuration_logo entete_dept { - display: inline-block; + display: inline-block; } .configuration_logo .effectifs { @@ -3971,4 +3971,18 @@ table.evaluations_recap td.nb_abs, table.evaluations_recap td.nb_att, table.evaluations_recap td.nb_exc { text-align: center; +} + +/* ------------- Tableau récap formation ------------ */ +table.formation_table_recap tr.ue td { + font-weight: bold; +} + +table.formation_table_recap td.coef, +table.formation_table_recap td.ects, +table.formation_table_recap td.nb_moduleimpls, +table.formation_table_recap td.heures_cours, +table.formation_table_recap td.heures_td, +table.formation_table_recap td.heures_tp { + text-align: right; } \ No newline at end of file diff --git a/app/static/js/formation_recap.js b/app/static/js/formation_recap.js new file mode 100644 index 000000000..0bcb009f4 --- /dev/null +++ b/app/static/js/formation_recap.js @@ -0,0 +1,28 @@ +/* Page accueil département */ +var apo_ue_editor = null; +var apo_mod_editor = null; + +$(document).ready(function () { + var table_options = { + "paging": false, + "searching": false, + "info": false, + /* "autoWidth" : false, */ + "fixedHeader": { + "header": true, + "footer": true + }, + "orderCellsTop": true, // cellules ligne 1 pour tri + "aaSorting": [], // Prevent initial sorting + }; + $('table#formation_table_recap').DataTable(table_options); + let table_editable = document.querySelector("table#formation_table_recap.apo_editable"); + if (table_editable) { + let apo_ue_save_url = document.querySelector("table#formation_table_recap.apo_editable").dataset.apo_ue_save_url; + apo_ue_editor = new ScoFieldEditor("table#formation_table_recap tr.ue td.apo", apo_ue_save_url, false); + let apo_mod_save_url = document.querySelector("table#formation_table_recap.apo_editable").dataset.apo_mod_save_url; + apo_mod_editor = new ScoFieldEditor("table#formation_table_recap tr.mod td.apo", apo_mod_save_url, false); + } +}); + + diff --git a/app/static/js/scolar_index.js b/app/static/js/scolar_index.js index 4718e467e..600a7cf5a 100644 --- a/app/static/js/scolar_index.js +++ b/app/static/js/scolar_index.js @@ -15,8 +15,11 @@ $(document).ready(function () { "aaSorting": [], // Prevent initial sorting }; $('table.semlist').DataTable(table_options); - let apo_save_url = document.querySelector("table#semlist.apo_editable").dataset.apo_save_url; - apo_editor = new ScoFieldEditor(".etapes_apo_str", apo_save_url, false); + let table_editable = document.querySelector("table#semlist.apo_editable"); + if (table_editable) { + let apo_save_url = document.querySelector("table#semlist.apo_editable").dataset.apo_save_url; + apo_editor = new ScoFieldEditor(".etapes_apo_str", apo_save_url, false); + } }); diff --git a/app/views/notes.py b/app/views/notes.py index ba0ca78e5..4fc69bdc8 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -45,11 +45,13 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestreUEComputationExpr +from app.models.modules import Module from app.models.ues import UniteEns from app import api from app import db from app import models +from app.models import ScolarNews from app.auth.models import User from app.but import bulletin_but from app.decorators import ( @@ -86,7 +88,6 @@ from app.scodoc import sco_archives from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_cache -from app.scodoc import sco_compute_moy from app.scodoc import sco_cost_formation from app.scodoc import sco_debouche from app.scodoc import sco_edit_apc @@ -103,6 +104,7 @@ from app.scodoc import sco_evaluation_edit from app.scodoc import sco_evaluation_recap from app.scodoc import sco_export_results from app.scodoc import sco_formations +from app.scodoc import sco_formation_recap from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_custommenu from app.scodoc import sco_formsemestre_edit @@ -480,7 +482,14 @@ sco_publish( methods=["GET", "POST"], ) sco_publish( - "/formation_table_recap", sco_edit_ue.formation_table_recap, Permission.ScoView + "/formation_table_recap", + sco_formation_recap.formation_table_recap, + Permission.ScoView, +) +sco_publish( + "/export_recap_formations_annee_scolaire", + sco_formation_recap.export_recap_formations_annee_scolaire, + Permission.ScoView, ) sco_publish( "/formation_add_malus_modules", @@ -571,6 +580,20 @@ def index_html():
  • Importer une formation (xml)
  • +
  • exporter les formations de l'année scolaire + {scu.AnneeScolaire()-1} - {scu.AnneeScolaire()} + +
  • +
  • exporter les formations de l'année scolaire + {scu.AnneeScolaire()} - {scu.AnneeScolaire()+1} + +
  • Référentiels de compétences