2020-09-26 16:19:37 +02:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# Gestion scolarite IUT
|
|
|
|
#
|
2023-12-31 23:04:06 +01:00
|
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
"""Import / Export de formations
|
|
|
|
"""
|
2021-02-03 22:00:41 +01:00
|
|
|
import xml.dom.minidom
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-08-01 10:16:16 +02:00
|
|
|
import flask
|
2024-02-14 21:45:58 +01:00
|
|
|
from flask import flash, g, request, url_for
|
2021-09-18 13:42:19 +02:00
|
|
|
from flask_login import current_user
|
2021-08-01 10:16:16 +02:00
|
|
|
|
2021-06-19 23:21:37 +02:00
|
|
|
import app.scodoc.sco_utils as scu
|
2021-11-30 11:07:17 +01:00
|
|
|
from app import db
|
2021-08-29 19:57:32 +02:00
|
|
|
from app import log
|
2024-10-11 14:06:02 +02:00
|
|
|
from app.formations import edit_module, edit_ue
|
|
|
|
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
|
2022-04-12 17:12:51 +02:00
|
|
|
from app.models import ScolarNews
|
2022-10-24 23:18:45 +02:00
|
|
|
from app.models.but_refcomp import (
|
|
|
|
ApcAppCritique,
|
|
|
|
ApcCompetence,
|
|
|
|
ApcNiveau,
|
|
|
|
ApcParcours,
|
|
|
|
ApcReferentielCompetences,
|
|
|
|
)
|
2022-10-23 23:28:24 +02:00
|
|
|
from app.scodoc import sco_cache
|
2023-02-12 13:36:47 +01:00
|
|
|
from app.scodoc import codes_cursus
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc import sco_preferences
|
2021-06-21 10:17:16 +02:00
|
|
|
from app.scodoc import sco_tag_module
|
2021-07-13 09:38:31 +02:00
|
|
|
from app.scodoc import sco_xml
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc.gen_tables import GenTable
|
2021-12-03 14:13:49 +01:00
|
|
|
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
2021-06-19 23:21:37 +02:00
|
|
|
from app.scodoc.sco_permissions import Permission
|
2024-10-10 00:41:20 +02:00
|
|
|
import sco_version
|
2021-06-16 18:18:32 +02:00
|
|
|
|
|
|
|
|
2023-03-21 21:14:38 +01:00
|
|
|
def formation_export_dict(
|
|
|
|
formation: Formation,
|
2021-10-24 11:43:53 +02:00
|
|
|
export_ids=False,
|
|
|
|
export_tags=True,
|
|
|
|
export_external_ues=False,
|
2023-01-25 15:17:52 +01:00
|
|
|
export_codes_apo=True,
|
2023-03-21 21:14:38 +01:00
|
|
|
ac_as_list=False,
|
|
|
|
ue_reference_style="id",
|
|
|
|
) -> dict:
|
|
|
|
"""Get a formation, with UE, matieres, modules...
|
|
|
|
as a deep dict.
|
|
|
|
ac_as_list spécifie le format des Appentissages Critiques.
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2022-10-24 23:18:45 +02:00
|
|
|
f_dict = formation.to_dict(with_refcomp_attrs=True)
|
2023-01-25 15:17:52 +01:00
|
|
|
if not export_ids:
|
2023-01-31 18:58:24 +01:00
|
|
|
del f_dict["id"]
|
2023-01-25 15:17:52 +01:00
|
|
|
del f_dict["formation_id"]
|
|
|
|
del f_dict["dept_id"]
|
|
|
|
ues = formation.ues
|
2021-10-24 11:43:53 +02:00
|
|
|
if not export_external_ues:
|
2023-01-25 15:17:52 +01:00
|
|
|
ues = ues.filter_by(is_external=False)
|
|
|
|
ues = ues.all()
|
|
|
|
ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme))
|
|
|
|
f_dict["ue"] = []
|
2024-01-26 14:57:50 +01:00
|
|
|
ue: UniteEns
|
2023-01-25 15:17:52 +01:00
|
|
|
for ue in ues:
|
|
|
|
ue_dict = ue.to_dict()
|
|
|
|
f_dict["ue"].append(ue_dict)
|
|
|
|
ue_dict.pop("module_ue_coefs", None)
|
2022-10-24 23:18:45 +02:00
|
|
|
if formation.is_apc():
|
|
|
|
# BUT: indique niveau de compétence associé à l'UE
|
|
|
|
if ue.niveau_competence:
|
2024-07-10 18:27:13 +02:00
|
|
|
ue_dict["apc_niveau_competence_titre"] = (
|
|
|
|
ue.niveau_competence.competence.titre
|
|
|
|
)
|
2022-10-24 23:18:45 +02:00
|
|
|
ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
|
|
|
|
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
|
|
|
|
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
|
2023-05-14 15:00:15 +02:00
|
|
|
|
2023-03-21 21:14:38 +01:00
|
|
|
# pour les coefficients:
|
|
|
|
ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
|
2020-09-26 16:19:37 +02:00
|
|
|
if not export_ids:
|
2023-01-25 15:17:52 +01:00
|
|
|
for id_id in (
|
|
|
|
"id",
|
|
|
|
"ue_id",
|
|
|
|
"formation_id",
|
|
|
|
"parcour_id",
|
|
|
|
"niveau_competence_id",
|
|
|
|
):
|
|
|
|
ue_dict.pop(id_id, None)
|
|
|
|
|
|
|
|
if not export_codes_apo:
|
|
|
|
ue_dict.pop("code_apogee", None)
|
2024-06-09 15:18:03 +02:00
|
|
|
ue_dict.pop("code_apogee_rcue", None)
|
2024-01-26 16:14:08 +01:00
|
|
|
if ue_dict.get("ects") is None:
|
|
|
|
ue_dict.pop("ects", None)
|
2024-10-11 14:06:02 +02:00
|
|
|
mats = ue.matieres.all()
|
|
|
|
mats.sort(key=lambda m: m.numero)
|
|
|
|
mats_dict = [mat.to_dict() for mat in mats]
|
|
|
|
ue_dict["matiere"] = mats_dict
|
|
|
|
for mat_d in mats_dict:
|
|
|
|
matiere_id = mat_d["matiere_id"]
|
2020-09-26 16:19:37 +02:00
|
|
|
if not export_ids:
|
2024-10-11 14:06:02 +02:00
|
|
|
del mat_d["id"]
|
|
|
|
del mat_d["matiere_id"]
|
|
|
|
del mat_d["ue_id"]
|
2024-10-10 00:41:20 +02:00
|
|
|
mods = edit_module.module_list({"matiere_id": matiere_id})
|
2023-01-25 15:17:52 +01:00
|
|
|
mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
|
2024-10-11 14:06:02 +02:00
|
|
|
mat_d["module"] = mods
|
|
|
|
for mod_d in mods:
|
|
|
|
module: Module = db.session.get(Module, mod_d["module_id"])
|
2020-09-26 16:19:37 +02:00
|
|
|
if export_tags:
|
2024-10-11 14:06:02 +02:00
|
|
|
tags = [t.title for t in module.tags]
|
2020-09-26 16:19:37 +02:00
|
|
|
if tags:
|
2024-10-11 14:06:02 +02:00
|
|
|
mod_d["tags"] = [{"name": x} for x in tags]
|
2021-11-30 11:07:17 +01:00
|
|
|
#
|
|
|
|
if module.is_apc():
|
|
|
|
# Exporte les coefficients
|
2023-03-21 21:14:38 +01:00
|
|
|
if ue_reference_style == "id":
|
2024-10-11 14:06:02 +02:00
|
|
|
mod_d["coefficients"] = [
|
2023-03-21 21:14:38 +01:00
|
|
|
{"ue_reference": str(ue_id), "coef": str(coef)}
|
|
|
|
for (ue_id, coef) in module.get_ue_coef_dict().items()
|
|
|
|
]
|
|
|
|
else:
|
2024-10-11 14:06:02 +02:00
|
|
|
mod_d["coefficients"] = [
|
2023-03-21 21:14:38 +01:00
|
|
|
{"ue_reference": ue_acronyme, "coef": str(coef)}
|
|
|
|
for (
|
|
|
|
ue_acronyme,
|
|
|
|
coef,
|
|
|
|
) in module.get_ue_coef_dict_acronyme().items()
|
|
|
|
]
|
2022-08-31 12:02:19 +02:00
|
|
|
# Et les parcours
|
2024-10-11 14:06:02 +02:00
|
|
|
mod_d["parcours"] = [
|
2022-08-31 12:02:19 +02:00
|
|
|
p.to_dict(with_annees=False) for p in module.parcours
|
|
|
|
]
|
2022-09-07 10:07:46 +02:00
|
|
|
# Et les AC
|
2023-03-21 21:14:38 +01:00
|
|
|
if ac_as_list:
|
2022-10-24 23:18:45 +02:00
|
|
|
# XML préfère une liste
|
2024-10-11 14:06:02 +02:00
|
|
|
mod_d["app_critiques"] = [
|
2022-10-24 23:18:45 +02:00
|
|
|
x.to_dict(with_code=True) for x in module.app_critiques
|
|
|
|
]
|
|
|
|
else:
|
2024-10-11 14:06:02 +02:00
|
|
|
mod_d["app_critiques"] = {
|
2022-10-24 23:18:45 +02:00
|
|
|
x.code: x.to_dict() for x in module.app_critiques
|
|
|
|
}
|
2020-09-26 16:19:37 +02:00
|
|
|
if not export_ids:
|
2024-10-11 14:06:02 +02:00
|
|
|
del mod_d["id"]
|
|
|
|
del mod_d["ue_id"]
|
|
|
|
del mod_d["matiere_id"]
|
|
|
|
del mod_d["module_id"]
|
|
|
|
del mod_d["formation_id"]
|
2023-01-25 15:17:52 +01:00
|
|
|
if not export_codes_apo:
|
2024-10-11 14:06:02 +02:00
|
|
|
del mod_d["code_apogee"]
|
|
|
|
if mod_d["ects"] is None:
|
|
|
|
del mod_d["ects"]
|
2023-03-21 21:14:38 +01:00
|
|
|
return f_dict
|
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2023-03-21 21:14:38 +01:00
|
|
|
def formation_export(
|
|
|
|
formation_id,
|
|
|
|
export_ids=False,
|
|
|
|
export_tags=True,
|
|
|
|
export_external_ues=False,
|
|
|
|
export_codes_apo=True,
|
2023-09-21 10:20:19 +02:00
|
|
|
fmt=None,
|
2024-10-04 08:46:09 +02:00
|
|
|
) -> flask.Response | dict:
|
2023-03-21 21:14:38 +01:00
|
|
|
"""Get a formation, with UE, matieres, modules
|
|
|
|
in desired format
|
|
|
|
"""
|
2024-09-30 03:21:22 +02:00
|
|
|
if fmt not in (None, "xml", "json"):
|
2024-09-27 22:37:54 +02:00
|
|
|
raise ScoValueError("Format invalide")
|
2024-07-10 18:27:13 +02:00
|
|
|
formation = Formation.get_formation(formation_id)
|
2023-03-21 21:14:38 +01:00
|
|
|
f_dict = formation_export_dict(
|
|
|
|
formation,
|
|
|
|
export_ids=export_ids,
|
|
|
|
export_tags=export_tags,
|
|
|
|
export_external_ues=export_external_ues,
|
|
|
|
export_codes_apo=export_codes_apo,
|
2023-09-21 10:20:19 +02:00
|
|
|
ac_as_list=fmt == "xml",
|
2023-03-21 21:14:38 +01:00
|
|
|
)
|
2024-10-04 08:46:09 +02:00
|
|
|
if fmt is None:
|
|
|
|
return f_dict
|
|
|
|
|
2022-03-01 09:48:37 +01:00
|
|
|
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
|
2021-10-16 19:20:36 +02:00
|
|
|
return scu.sendResult(
|
2022-10-24 23:18:45 +02:00
|
|
|
f_dict,
|
2022-03-01 09:48:37 +01:00
|
|
|
name="formation",
|
2023-09-21 10:20:19 +02:00
|
|
|
fmt=fmt,
|
2022-03-01 09:48:37 +01:00
|
|
|
force_outer_xml_tag=False,
|
|
|
|
attached=True,
|
|
|
|
filename=filename,
|
2021-10-16 19:20:36 +02:00
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2022-10-24 23:18:45 +02:00
|
|
|
def _formation_retreive_refcomp(f_dict: dict) -> int:
|
|
|
|
"""Recherche si on un référentiel de compétence chargé pour
|
|
|
|
cette formation: utilise comme clé (version_orebut, specialite, type_titre)
|
|
|
|
Retourne: referentiel_competence_id ou None
|
|
|
|
"""
|
|
|
|
refcomp_version_orebut = f_dict.get("refcomp_version_orebut")
|
|
|
|
refcomp_specialite = f_dict.get("refcomp_specialite")
|
|
|
|
refcomp_type_titre = f_dict.get("refcomp_type_titre")
|
|
|
|
if all((refcomp_version_orebut, refcomp_specialite, refcomp_type_titre)):
|
|
|
|
refcomp = ApcReferentielCompetences.query.filter_by(
|
|
|
|
dept_id=g.scodoc_dept_id,
|
|
|
|
type_titre=refcomp_type_titre,
|
|
|
|
specialite=refcomp_specialite,
|
|
|
|
version_orebut=refcomp_version_orebut,
|
|
|
|
).first()
|
|
|
|
if refcomp:
|
|
|
|
return refcomp.id
|
|
|
|
else:
|
|
|
|
flash(
|
2024-07-10 18:27:13 +02:00
|
|
|
f"""Impossible de trouver le référentiel de compétence pour {
|
|
|
|
refcomp_specialite} : est-il chargé ?"""
|
2022-10-24 23:18:45 +02:00
|
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _formation_retreive_apc_niveau(
|
|
|
|
referentiel_competence_id: int, ue_dict: dict
|
2024-07-10 18:27:13 +02:00
|
|
|
) -> int | None:
|
2023-04-19 11:56:33 +02:00
|
|
|
"""Recherche dans le ref. de comp. un niveau pour cette UE.
|
2024-07-10 18:27:13 +02:00
|
|
|
Utilise (libelle, annee, ordre) comme clé, ou
|
|
|
|
(competence_titre, libelle, annee, ordre) si présent.
|
2022-10-24 23:18:45 +02:00
|
|
|
"""
|
|
|
|
libelle = ue_dict.get("apc_niveau_libelle")
|
|
|
|
annee = ue_dict.get("apc_niveau_annee")
|
|
|
|
ordre = ue_dict.get("apc_niveau_ordre")
|
2024-07-10 18:27:13 +02:00
|
|
|
competence_titre = ue_dict.get("apc_niveau_competence_titre")
|
|
|
|
niveau = None
|
|
|
|
if all((competence_titre, libelle, annee, ordre)):
|
|
|
|
niveau = (
|
|
|
|
ApcNiveau.query.filter_by(libelle=libelle, annee=annee, ordre=ordre)
|
|
|
|
.join(ApcCompetence)
|
|
|
|
.filter_by(referentiel_id=referentiel_competence_id, titre=competence_titre)
|
|
|
|
).first()
|
|
|
|
|
|
|
|
elif all((libelle, annee, ordre)):
|
2022-10-24 23:18:45 +02:00
|
|
|
niveau = (
|
|
|
|
ApcNiveau.query.filter_by(libelle=libelle, annee=annee, ordre=ordre)
|
|
|
|
.join(ApcCompetence)
|
|
|
|
.filter_by(referentiel_id=referentiel_competence_id)
|
|
|
|
).first()
|
2024-07-10 18:27:13 +02:00
|
|
|
|
|
|
|
return niveau.id if niveau is not None else None
|
2022-10-24 23:18:45 +02:00
|
|
|
|
|
|
|
|
2024-09-27 22:37:54 +02:00
|
|
|
def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=False):
|
2020-09-26 16:19:37 +02:00
|
|
|
"""Create a formation from XML representation
|
2023-09-21 10:20:19 +02:00
|
|
|
(format dumped by formation_export( fmt='xml' ))
|
2021-08-01 10:16:16 +02:00
|
|
|
XML may contain object (UE, modules) ids: this function returns two
|
|
|
|
dicts mapping these ids to the created ids.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
doc: str, xml data
|
|
|
|
import_tags: if false, does not import tags on modules.
|
2022-10-23 23:28:24 +02:00
|
|
|
use_local_refcomp: if True, utilise les id vers les ref. de compétences.
|
2021-08-01 10:16:16 +02:00
|
|
|
Returns:
|
|
|
|
formation_id, modules_old2new, ues_old2new
|
2020-09-26 16:19:37 +02:00
|
|
|
"""
|
2024-10-10 00:41:20 +02:00
|
|
|
from app.formations import edit_formation
|
2021-06-21 10:17:16 +02:00
|
|
|
|
2024-09-27 22:37:54 +02:00
|
|
|
if isinstance(doc, bytes):
|
|
|
|
doc = doc.decode(scu.SCO_ENCODING)
|
2020-09-26 16:19:37 +02:00
|
|
|
try:
|
2024-09-27 22:37:54 +02:00
|
|
|
dom = xml.dom.minidom.parseString(sco_xml.remove_control_characters(doc))
|
2022-08-05 07:03:35 +02:00
|
|
|
except Exception as exc:
|
2024-09-27 22:37:54 +02:00
|
|
|
log(f"formation_import_xml: invalid XML data:\n{exc}")
|
|
|
|
raise ScoValueError(f"Fichier XML invalide {exc}") from exc
|
2020-09-26 16:19:37 +02:00
|
|
|
|
2021-12-03 10:46:14 +01:00
|
|
|
try:
|
|
|
|
f = dom.getElementsByTagName("formation")[0] # or dom.documentElement
|
|
|
|
D = sco_xml.xml_to_dicts(f)
|
2022-08-05 07:03:35 +02:00
|
|
|
except Exception as exc:
|
2021-12-03 14:13:49 +01:00
|
|
|
raise ScoFormatError(
|
2023-11-19 22:06:36 +01:00
|
|
|
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
|
2021-12-03 14:13:49 +01:00
|
|
|
(élément 'formation' inexistant par exemple)."""
|
2022-08-05 07:03:35 +02:00
|
|
|
) from exc
|
2020-09-26 16:19:37 +02:00
|
|
|
assert D[0] == "formation"
|
2022-10-24 23:18:45 +02:00
|
|
|
f_dict = D[1]
|
|
|
|
f_dict["dept_id"] = g.scodoc_dept_id
|
2022-10-23 23:28:24 +02:00
|
|
|
# Pour les clonages, on prend le refcomp_id donné:
|
|
|
|
referentiel_competence_id = (
|
2022-10-24 23:18:45 +02:00
|
|
|
f_dict.get("referentiel_competence_id") if use_local_refcomp else None
|
2022-10-23 23:28:24 +02:00
|
|
|
)
|
|
|
|
# Sinon, on cherche a retrouver le ref. comp.
|
|
|
|
if referentiel_competence_id is None:
|
2022-10-24 23:18:45 +02:00
|
|
|
referentiel_competence_id = _formation_retreive_refcomp(f_dict)
|
|
|
|
f_dict["referentiel_competence_id"] = referentiel_competence_id
|
2020-09-26 16:19:37 +02:00
|
|
|
# find new version number
|
2023-11-20 11:23:40 +01:00
|
|
|
acronyme_lower = f_dict["acronyme"].lower() if f_dict["acronyme"] else ""
|
|
|
|
titre_lower = f_dict["titre"].lower() if f_dict["titre"] else ""
|
2023-02-21 14:57:16 +01:00
|
|
|
formations: list[Formation] = Formation.query.filter_by(
|
2023-11-19 22:06:36 +01:00
|
|
|
dept_id=f_dict["dept_id"]
|
|
|
|
).filter(
|
|
|
|
db.func.lower(Formation.acronyme) == acronyme_lower,
|
2023-11-20 11:23:40 +01:00
|
|
|
db.func.lower(Formation.titre) == titre_lower,
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2023-02-18 00:13:00 +01:00
|
|
|
if formations.count():
|
2023-02-21 14:57:16 +01:00
|
|
|
version = max(f.version or 0 for f in formations)
|
2022-10-23 23:28:24 +02:00
|
|
|
else:
|
|
|
|
version = 0
|
2022-10-24 23:18:45 +02:00
|
|
|
f_dict["version"] = version + 1
|
2022-10-23 23:28:24 +02:00
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
# create formation
|
2024-10-10 00:41:20 +02:00
|
|
|
formation = edit_formation.do_formation_create(f_dict)
|
2023-02-20 21:04:29 +01:00
|
|
|
log(f"formation {formation.id} created")
|
2022-10-23 23:28:24 +02:00
|
|
|
|
2020-09-26 16:19:37 +02:00
|
|
|
ues_old2new = {} # xml ue_id : new ue_id
|
|
|
|
modules_old2new = {} # xml module_id : new module_id
|
|
|
|
# (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML)
|
2021-11-30 11:07:17 +01:00
|
|
|
|
|
|
|
ue_reference_to_id = {} # pour les coefs APC (map reference -> ue_id)
|
|
|
|
modules_a_coefficienter = [] # Liste des modules avec coefs APC
|
2022-10-23 23:28:24 +02:00
|
|
|
with sco_cache.DeferredSemCacheManager():
|
|
|
|
# -- create UEs
|
|
|
|
for ue_info in D[2]:
|
|
|
|
assert ue_info[0] == "ue"
|
2023-02-20 21:04:29 +01:00
|
|
|
ue_info[1]["formation_id"] = formation.id
|
2022-10-23 23:28:24 +02:00
|
|
|
if "ue_id" in ue_info[1]:
|
|
|
|
xml_ue_id = int(ue_info[1]["ue_id"])
|
|
|
|
del ue_info[1]["ue_id"]
|
|
|
|
else:
|
|
|
|
xml_ue_id = None
|
2022-10-24 23:18:45 +02:00
|
|
|
if referentiel_competence_id is None:
|
|
|
|
if "niveau_competence_id" in ue_info[1]:
|
|
|
|
del ue_info[1]["niveau_competence_id"]
|
|
|
|
else:
|
|
|
|
ue_info[1]["niveau_competence_id"] = _formation_retreive_apc_niveau(
|
|
|
|
referentiel_competence_id, ue_info[1]
|
|
|
|
)
|
2024-01-26 14:57:50 +01:00
|
|
|
# Note: si le code est indiqué "" dans le xml, il faut le conserver vide
|
|
|
|
# pour la comparaison ultérieure des formations XXX
|
2024-10-10 00:41:20 +02:00
|
|
|
ue_id = edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
|
2023-07-11 06:57:38 +02:00
|
|
|
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
2023-01-25 15:17:52 +01:00
|
|
|
assert ue
|
2022-10-23 23:28:24 +02:00
|
|
|
if xml_ue_id:
|
|
|
|
ues_old2new[xml_ue_id] = ue_id
|
2023-05-14 15:00:15 +02:00
|
|
|
|
2022-10-23 23:28:24 +02:00
|
|
|
# élément optionnel présent dans les exports BUT:
|
|
|
|
ue_reference = ue_info[1].get("reference")
|
|
|
|
if ue_reference:
|
|
|
|
ue_reference_to_id[int(ue_reference)] = ue_id
|
2023-01-25 15:17:52 +01:00
|
|
|
|
2023-05-14 15:00:15 +02:00
|
|
|
# -- Create matieres
|
2022-10-23 23:28:24 +02:00
|
|
|
for mat_info in ue_info[2]:
|
2023-04-03 17:46:31 +02:00
|
|
|
# Backward compat: un seul parcours par UE (ScoDoc < 9.4.71)
|
2023-01-25 15:17:52 +01:00
|
|
|
if mat_info[0] == "parcour":
|
|
|
|
# Parcours (BUT)
|
|
|
|
code_parcours = mat_info[1]["code"]
|
|
|
|
parcour = ApcParcours.query.filter_by(
|
|
|
|
code=code_parcours,
|
|
|
|
referentiel_id=referentiel_competence_id,
|
|
|
|
).first()
|
|
|
|
if parcour:
|
2023-04-03 17:46:31 +02:00
|
|
|
ue.parcours = [parcour]
|
2023-01-25 15:17:52 +01:00
|
|
|
db.session.add(ue)
|
|
|
|
else:
|
2023-04-03 17:46:31 +02:00
|
|
|
flash(f"Attention: parcours {code_parcours} inexistant !")
|
2023-01-25 15:17:52 +01:00
|
|
|
log(f"Warning: parcours {code_parcours} inexistant !")
|
|
|
|
continue
|
2023-04-03 17:46:31 +02:00
|
|
|
elif mat_info[0] == "parcours":
|
2023-05-14 15:00:15 +02:00
|
|
|
# Parcours (BUT), liste (ScoDoc > 9.4.70), avec ECTS en option
|
|
|
|
code_parcours = mat_info[1]["code"]
|
|
|
|
ue_parcour_ects = mat_info[1].get("ects")
|
|
|
|
parcour = ApcParcours.query.filter_by(
|
|
|
|
code=code_parcours,
|
|
|
|
referentiel_id=referentiel_competence_id,
|
|
|
|
).first()
|
|
|
|
if parcour:
|
|
|
|
ue.parcours.append(parcour)
|
|
|
|
else:
|
|
|
|
flash(f"Attention: parcours {code_parcours} inexistant !")
|
|
|
|
log(f"Warning: parcours {code_parcours} inexistant !")
|
|
|
|
if ue_parcour_ects is not None:
|
|
|
|
ue.set_ects(ue_parcour_ects, parcour)
|
2023-04-03 17:46:31 +02:00
|
|
|
db.session.add(ue)
|
|
|
|
continue
|
|
|
|
|
2022-10-23 23:28:24 +02:00
|
|
|
assert mat_info[0] == "matiere"
|
|
|
|
mat_info[1]["ue_id"] = ue_id
|
2024-10-11 14:06:02 +02:00
|
|
|
mat = Matiere.create_from_dict(mat_info[1])
|
|
|
|
mat_id = mat.id
|
2022-10-23 23:28:24 +02:00
|
|
|
# -- create modules
|
|
|
|
for mod_info in mat_info[2]:
|
|
|
|
assert mod_info[0] == "module"
|
|
|
|
if "module_id" in mod_info[1]:
|
|
|
|
xml_module_id = int(mod_info[1]["module_id"])
|
|
|
|
del mod_info[1]["module_id"]
|
|
|
|
else:
|
|
|
|
xml_module_id = None
|
2023-02-20 21:04:29 +01:00
|
|
|
mod_info[1]["formation_id"] = formation.id
|
2022-10-23 23:28:24 +02:00
|
|
|
mod_info[1]["matiere_id"] = mat_id
|
|
|
|
mod_info[1]["ue_id"] = ue_id
|
|
|
|
if not "module_type" in mod_info[1]:
|
|
|
|
mod_info[1]["module_type"] = scu.ModuleType.STANDARD
|
2024-10-10 00:41:20 +02:00
|
|
|
mod_id = edit_module.do_module_create(mod_info[1])
|
2022-10-23 23:28:24 +02:00
|
|
|
if xml_module_id:
|
|
|
|
modules_old2new[int(xml_module_id)] = mod_id
|
|
|
|
if len(mod_info) > 2:
|
2023-07-11 06:57:38 +02:00
|
|
|
module: Module = db.session.get(Module, mod_id)
|
2022-10-23 23:28:24 +02:00
|
|
|
tag_names = []
|
|
|
|
ue_coef_dict = {}
|
|
|
|
for child in mod_info[2]:
|
|
|
|
if child[0] == "tags" and import_tags:
|
|
|
|
tag_names.append(child[1]["name"])
|
|
|
|
elif child[0] == "coefficients":
|
|
|
|
ue_reference = int(child[1]["ue_reference"])
|
|
|
|
coef = float(child[1]["coef"])
|
|
|
|
ue_coef_dict[ue_reference] = coef
|
2022-10-24 23:18:45 +02:00
|
|
|
elif child[0] == "app_critiques" and (
|
|
|
|
referentiel_competence_id is not None
|
|
|
|
):
|
|
|
|
ac_code = child[1]["code"]
|
|
|
|
ac = (
|
|
|
|
ApcAppCritique.query.filter_by(code=ac_code)
|
|
|
|
.join(ApcNiveau)
|
|
|
|
.join(ApcCompetence)
|
|
|
|
.filter_by(referentiel_id=referentiel_competence_id)
|
|
|
|
).first()
|
|
|
|
if ac is not None:
|
|
|
|
module.app_critiques.append(ac)
|
|
|
|
db.session.add(module)
|
|
|
|
else:
|
|
|
|
log(f"Warning: AC {ac_code} inexistant !")
|
|
|
|
|
2022-10-23 23:28:24 +02:00
|
|
|
elif child[0] == "parcours":
|
|
|
|
# Si on a un référentiel de compétences,
|
|
|
|
# associe les parcours de ce module (BUT)
|
2022-10-24 23:18:45 +02:00
|
|
|
if referentiel_competence_id is not None:
|
2022-10-23 23:28:24 +02:00
|
|
|
code_parcours = child[1]["code"]
|
2023-01-25 15:17:52 +01:00
|
|
|
parcour = ApcParcours.query.filter_by(
|
2022-10-23 23:28:24 +02:00
|
|
|
code=code_parcours,
|
|
|
|
referentiel_id=referentiel_competence_id,
|
|
|
|
).first()
|
2023-01-25 15:17:52 +01:00
|
|
|
if parcour:
|
|
|
|
module.parcours.append(parcour)
|
2022-10-23 23:28:24 +02:00
|
|
|
db.session.add(module)
|
|
|
|
else:
|
|
|
|
log(
|
|
|
|
f"Warning: parcours {code_parcours} inexistant !"
|
|
|
|
)
|
|
|
|
if import_tags and tag_names:
|
2024-10-11 14:06:02 +02:00
|
|
|
module.set_tags(tag_names)
|
2022-10-23 23:28:24 +02:00
|
|
|
if module.is_apc() and ue_coef_dict:
|
|
|
|
modules_a_coefficienter.append((module, ue_coef_dict))
|
|
|
|
# Fixe les coefs APC (à la fin pour que les UE soient créées)
|
|
|
|
for module, ue_coef_dict_ref in modules_a_coefficienter:
|
|
|
|
# remap ue ids:
|
|
|
|
ue_coef_dict = {
|
|
|
|
ue_reference_to_id[k]: v for (k, v) in ue_coef_dict_ref.items()
|
|
|
|
}
|
|
|
|
module.set_ue_coef_dict(ue_coef_dict)
|
|
|
|
db.session.commit()
|
2023-02-20 21:04:29 +01:00
|
|
|
return formation.id, modules_old2new, ues_old2new
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
|
2024-04-05 00:23:29 +02:00
|
|
|
def formation_list_table(detail: bool) -> GenTable:
|
2020-09-26 16:19:37 +02:00
|
|
|
"""List formation, grouped by titre and sorted by versions
|
2024-04-05 00:23:29 +02:00
|
|
|
and listing associated semestres.
|
|
|
|
If detail, add column with more details.
|
2020-09-26 16:19:37 +02:00
|
|
|
returns a table
|
|
|
|
"""
|
2023-02-21 00:34:39 +01:00
|
|
|
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
|
2024-02-14 21:45:58 +01:00
|
|
|
title = "Formations (programmes pédagogiques)"
|
2021-02-04 20:02:44 +01:00
|
|
|
lockicon = scu.icontag(
|
2020-09-26 16:19:37 +02:00
|
|
|
"lock32_img", title="Comporte des semestres verrouillés", border="0"
|
|
|
|
)
|
2021-02-04 20:02:44 +01:00
|
|
|
suppricon = scu.icontag(
|
2020-09-26 16:19:37 +02:00
|
|
|
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
|
|
|
|
)
|
2021-02-04 20:02:44 +01:00
|
|
|
editicon = scu.icontag(
|
2020-09-26 16:19:37 +02:00
|
|
|
"edit_img", border="0", alt="modifier", title="Modifier titres et code"
|
|
|
|
)
|
|
|
|
|
2023-09-29 21:17:31 +02:00
|
|
|
editable = current_user.has_permission(Permission.EditFormation)
|
2024-03-27 17:12:28 +01:00
|
|
|
can_implement = current_user.has_permission(Permission.EditFormSemestre)
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
# Traduit/ajoute des champs à afficher:
|
2023-02-21 00:34:39 +01:00
|
|
|
rows = []
|
|
|
|
for formation in formations:
|
|
|
|
acronyme_no_spaces = formation.acronyme.lower().replace(" ", "-")
|
|
|
|
row = {
|
|
|
|
"acronyme": formation.acronyme,
|
|
|
|
"parcours_name": codes_cursus.get_cursus_from_code(
|
|
|
|
formation.type_parcours
|
|
|
|
).NAME,
|
|
|
|
"titre": formation.titre,
|
|
|
|
"_titre_target": url_for(
|
|
|
|
"notes.ue_table",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
formation_id=formation.id,
|
|
|
|
),
|
|
|
|
"_titre_link_class": "stdlink",
|
|
|
|
"_titre_id": f"""titre-{acronyme_no_spaces}""",
|
|
|
|
"version": formation.version or 0,
|
2023-11-19 22:06:36 +01:00
|
|
|
"commentaire": formation.commentaire or "",
|
2024-03-01 12:03:00 +01:00
|
|
|
"referentiel": (
|
|
|
|
f"""{formation.referentiel_competence.specialite} {
|
|
|
|
formation.referentiel_competence.get_version()}"""
|
|
|
|
if formation.referentiel_competence
|
|
|
|
else ""
|
|
|
|
),
|
2024-04-05 00:23:29 +02:00
|
|
|
"_referentiel_target": (
|
|
|
|
url_for(
|
|
|
|
"notes.refcomp_show",
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
refcomp_id=formation.referentiel_competence.id,
|
|
|
|
)
|
|
|
|
if formation.referentiel_competence
|
|
|
|
else ""
|
|
|
|
),
|
2023-02-21 00:34:39 +01:00
|
|
|
}
|
2020-09-26 16:19:37 +02:00
|
|
|
# Ajoute les semestres associés à chaque formation:
|
2023-02-21 00:34:39 +01:00
|
|
|
row["formsemestres"] = formation.formsemestres.order_by(
|
|
|
|
FormSemestre.date_debut
|
|
|
|
).all()
|
|
|
|
row["sems_list_txt"] = ", ".join(s.session_id() for s in row["formsemestres"])
|
2023-02-23 21:19:57 +01:00
|
|
|
row["_sems_list_txt_html"] = ", ".join(
|
|
|
|
[
|
2023-02-21 00:34:39 +01:00
|
|
|
f"""<a class="discretelink" href="{
|
|
|
|
url_for("notes.formsemestre_status",
|
|
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=s.id
|
2023-02-23 21:19:57 +01:00
|
|
|
)}">{s.session_id()}</a>"""
|
2023-02-21 00:34:39 +01:00
|
|
|
for s in row["formsemestres"]
|
2023-02-23 21:19:57 +01:00
|
|
|
]
|
2024-03-27 17:12:28 +01:00
|
|
|
+ (
|
|
|
|
[
|
|
|
|
f"""<a class="stdlink"
|
2023-02-21 21:34:38 +01:00
|
|
|
href="{ url_for("notes.formsemestre_createwithmodules",
|
2023-02-21 00:34:39 +01:00
|
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
|
|
|
|
)
|
|
|
|
}">ajouter</a>
|
|
|
|
"""
|
2024-03-27 17:12:28 +01:00
|
|
|
]
|
|
|
|
if can_implement
|
|
|
|
else []
|
|
|
|
)
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2024-04-05 00:23:29 +02:00
|
|
|
# Répartition des UEs dans les semestres
|
|
|
|
# utilise pour voir si la formation couvre tous les semestres
|
|
|
|
row["semestres_ues"] = ", ".join(
|
|
|
|
"S" + str(x if (x is not None and x > 0) else "-")
|
2024-04-05 10:11:34 +02:00
|
|
|
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
|
2024-04-05 00:23:29 +02:00
|
|
|
)
|
|
|
|
# Date surtout utilisées pour le tri:
|
2023-02-21 00:34:39 +01:00
|
|
|
if row["formsemestres"]:
|
2024-04-05 00:23:29 +02:00
|
|
|
row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
|
2023-02-21 00:34:39 +01:00
|
|
|
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
2023-02-21 00:34:39 +01:00
|
|
|
row["date_fin_dernier_sem"] = ""
|
|
|
|
row["annee_dernier_sem"] = 0
|
2020-09-26 16:19:37 +02:00
|
|
|
#
|
2023-02-21 00:34:39 +01:00
|
|
|
if formation.has_locked_sems():
|
2020-09-26 16:19:37 +02:00
|
|
|
but_locked = lockicon
|
2023-02-21 00:34:39 +01:00
|
|
|
but_suppr = '<span class="but_placeholder"></span>'
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
|
|
|
but_locked = '<span class="but_placeholder"></span>'
|
2023-02-21 00:34:39 +01:00
|
|
|
if editable:
|
|
|
|
but_suppr = f"""<a class="stdlink" href="{
|
|
|
|
url_for("notes.formation_delete",
|
|
|
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id
|
|
|
|
)}" id="delete-formation-{acronyme_no_spaces}">{suppricon}</a>"""
|
|
|
|
else:
|
|
|
|
but_suppr = '<span class="but_placeholder"></span>'
|
2020-09-26 16:19:37 +02:00
|
|
|
if editable:
|
2023-02-21 00:34:39 +01:00
|
|
|
but_edit = f"""<a class="stdlink" href="{
|
|
|
|
url_for("notes.formation_edit", scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
|
|
|
}" id="edit-formation-{acronyme_no_spaces}">{editicon}</a>"""
|
2020-09-26 16:19:37 +02:00
|
|
|
else:
|
|
|
|
but_edit = '<span class="but_placeholder"></span>'
|
2023-02-21 00:34:39 +01:00
|
|
|
row["buttons"] = ""
|
|
|
|
row["_buttons_html"] = but_locked + but_suppr + but_edit
|
|
|
|
rows.append(row)
|
|
|
|
# Tri par annee_dernier_sem, type, acronyme, titre, version décroissante
|
2023-03-26 10:08:50 +02:00
|
|
|
# donc plus récemment utilisée en tête
|
2023-02-21 00:34:39 +01:00
|
|
|
rows.sort(
|
|
|
|
key=lambda row: (
|
|
|
|
-row["annee_dernier_sem"],
|
|
|
|
row["parcours_name"],
|
|
|
|
row["acronyme"],
|
|
|
|
row["titre"],
|
|
|
|
-row["version"],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
for i, row in enumerate(rows):
|
|
|
|
row["_buttons_order"] = f"{i:05d}"
|
2020-09-26 16:19:37 +02:00
|
|
|
|
|
|
|
#
|
|
|
|
columns_ids = (
|
|
|
|
"buttons",
|
|
|
|
"acronyme",
|
|
|
|
"parcours_name",
|
|
|
|
"formation_code",
|
|
|
|
"version",
|
|
|
|
"titre",
|
2024-03-01 12:03:00 +01:00
|
|
|
"referentiel",
|
2023-11-19 22:06:36 +01:00
|
|
|
"commentaire",
|
2020-09-26 16:19:37 +02:00
|
|
|
"sems_list_txt",
|
|
|
|
)
|
2024-04-05 00:23:29 +02:00
|
|
|
if detail:
|
|
|
|
columns_ids += ("annee_dernier_sem", "semestres_ues")
|
2020-09-26 16:19:37 +02:00
|
|
|
titles = {
|
|
|
|
"buttons": "",
|
2023-11-19 22:06:36 +01:00
|
|
|
"commentaire": "Commentaire",
|
2020-09-26 16:19:37 +02:00
|
|
|
"acronyme": "Acro.",
|
|
|
|
"parcours_name": "Type",
|
|
|
|
"titre": "Titre",
|
|
|
|
"version": "Version",
|
|
|
|
"formation_code": "Code",
|
|
|
|
"sems_list_txt": "Semestres",
|
2024-08-23 08:39:53 +02:00
|
|
|
"referentiel": "Réf. Comp.",
|
2024-04-05 00:23:29 +02:00
|
|
|
"date_fin_dernier_sem": "Fin dernier sem.",
|
|
|
|
"annee_dernier_sem": "Année dernier sem.",
|
|
|
|
"semestres_ues": "Semestres avec UEs",
|
2020-09-26 16:19:37 +02:00
|
|
|
}
|
|
|
|
return GenTable(
|
2024-05-22 00:06:30 +02:00
|
|
|
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
|
2020-09-26 16:19:37 +02:00
|
|
|
caption=title,
|
2024-05-22 00:06:30 +02:00
|
|
|
columns_ids=columns_ids,
|
2020-09-26 16:19:37 +02:00
|
|
|
html_caption=title,
|
|
|
|
html_class="formation_list_table table_leftalign",
|
|
|
|
html_sortable=True,
|
2024-05-22 00:06:30 +02:00
|
|
|
html_with_td_classes=True,
|
|
|
|
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
2020-09-26 16:19:37 +02:00
|
|
|
page_title=title,
|
|
|
|
pdf_title=title,
|
2021-07-28 17:03:54 +02:00
|
|
|
preferences=sco_preferences.SemPreferences(),
|
2024-05-22 00:06:30 +02:00
|
|
|
rows=rows,
|
|
|
|
table_id="formation_list_table",
|
|
|
|
titles=titles,
|
2020-09-26 16:19:37 +02:00
|
|
|
)
|
2021-06-21 10:17:16 +02:00
|
|
|
|
|
|
|
|
2021-09-24 20:20:45 +02:00
|
|
|
def formation_create_new_version(formation_id, redirect=True):
|
2021-06-21 10:17:16 +02:00
|
|
|
"duplicate formation, with new version number"
|
2022-07-13 19:23:55 +02:00
|
|
|
formation = Formation.query.get_or_404(formation_id)
|
2023-06-13 21:28:37 +02:00
|
|
|
resp = formation_export(
|
2023-09-21 10:20:19 +02:00
|
|
|
formation_id, export_ids=True, export_external_ues=True, fmt="xml"
|
2023-06-13 21:28:37 +02:00
|
|
|
)
|
2021-09-24 20:20:45 +02:00
|
|
|
xml_data = resp.get_data(as_text=True)
|
2022-10-23 23:28:24 +02:00
|
|
|
new_id, modules_old2new, ues_old2new = formation_import_xml(
|
|
|
|
xml_data, use_local_refcomp=True
|
|
|
|
)
|
2021-06-21 10:17:16 +02:00
|
|
|
# news
|
2022-04-12 17:12:51 +02:00
|
|
|
ScolarNews.add(
|
|
|
|
typ=ScolarNews.NEWS_FORM,
|
|
|
|
obj=new_id,
|
2022-07-13 19:23:55 +02:00
|
|
|
text=f"Nouvelle version de la formation {formation.acronyme}",
|
2023-06-23 10:37:32 +02:00
|
|
|
max_frequency=0,
|
2021-06-21 10:17:16 +02:00
|
|
|
)
|
|
|
|
if redirect:
|
2022-07-13 19:23:55 +02:00
|
|
|
flash("Nouvelle version !")
|
2021-07-31 18:01:10 +02:00
|
|
|
return flask.redirect(
|
2021-08-09 10:09:04 +02:00
|
|
|
url_for(
|
2021-10-17 23:19:26 +02:00
|
|
|
"notes.ue_table",
|
2021-08-09 10:09:04 +02:00
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
formation_id=new_id,
|
|
|
|
msg="Nouvelle version !",
|
|
|
|
)
|
2021-06-21 10:17:16 +02:00
|
|
|
)
|
2023-03-21 21:14:38 +01:00
|
|
|
return new_id, modules_old2new, ues_old2new
|