This commit is contained in:
Emmanuel Viennet 2022-04-20 23:48:39 +02:00
commit 97d306d9d0
12 changed files with 369 additions and 124 deletions

View File

@ -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())

View File

@ -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:

View File

@ -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
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,

View File

@ -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</%s>"
% (
elem,
std,
row.get("_%s_td_attrs" % cid, ""),
attrs,
klass,
colspan_txt,
content,

View File

@ -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="<h2>" + title + "</h2>",
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.

View File

@ -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"<h2>{title}</h2>",
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,
)

View File

@ -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

View File

@ -3972,3 +3972,17 @@ 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;
}

View File

@ -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);
}
});

View File

@ -15,8 +15,11 @@ $(document).ready(function () {
"aaSorting": [], // Prevent initial sorting
};
$('table.semlist').DataTable(table_options);
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);
}
});

View File

@ -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():
</li>
<li><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.AnneeScolaire()-1)
}">exporter les formations de l'année scolaire
{scu.AnneeScolaire()-1} - {scu.AnneeScolaire()}
</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.AnneeScolaire())
}">exporter les formations de l'année scolaire
{scu.AnneeScolaire()} - {scu.AnneeScolaire()+1}
</a>
</li>
</ul>
<h3>Référentiels de compétences</h3>
<ul>
@ -2433,7 +2456,52 @@ def formsemestre_set_apo_etapes():
formsemestre.etapes.append(etape)
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
)
return ("", 204)
@bp.route("/ue_set_apo", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def ue_set_apo():
"""Change le code APO de l'UE
Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE)
"""
ue_id = int(request.form.get("oid"))
code_apo = (request.form.get("value") or "").strip()
ue = UniteEns.query.get_or_404(ue_id)
if code_apo != ue.code_apogee:
ue.code_apogee = code_apo
db.session.add(ue)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {ue.formation.titre} ({ue.formation.acronyme})",
)
return ("", 204)
@bp.route("/module_set_apo", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def module_set_apo():
"""Change le code APO du module
Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE)
"""
oid = int(request.form.get("oid"))
code_apo = (request.form.get("value") or "").strip()
mod = Module.query.get_or_404(oid)
if code_apo != mod.code_apogee:
mod.code_apogee = code_apo
db.session.add(mod)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {mod.formation.titre} ({mod.formation.acronyme})",
)
return ("", 204)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.2.5"
SCOVERSION = "9.2.6"
SCONAME = "ScoDoc"