Import formations / ref. comp. : fixes #510

This commit is contained in:
Emmanuel Viennet 2022-10-23 23:28:24 +02:00
parent a92d2a6edf
commit dac46b8366
5 changed files with 161 additions and 121 deletions

View File

@ -61,12 +61,19 @@ class Formation(db.Model):
"titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
def to_dict(self):
def to_dict(self, with_refcomp_attrs=False):
""" "as a dict.
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
e["departement"] = self.departement.to_dict()
# ScoDoc7 output_formators: (backward compat)
e["formation_id"] = self.id
e["formation_id"] = self.id # ScoDoc7 backward compat
if with_refcomp_attrs and self.referentiel_competence:
e["refcomp_version_orebut"] = self.referentiel_competence.version_orebut
e["refcomp_specialite"] = self.referentiel_competence.specialite
e["refcomp_type_titre"] = self.referentiel_competence.type_titre
return e
def get_parcours(self):

View File

@ -45,6 +45,7 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations
@ -117,15 +118,16 @@ def do_formation_delete(formation_id):
),
)
# Suppression des modules
for module in formation.modules:
db.session.delete(module)
db.session.flush()
# Suppression des UEs
for ue in formation.ues:
sco_edit_ue.do_ue_delete(ue, force=True)
with sco_cache.DeferredSemCacheManager():
# Suppression des modules
for module in formation.modules:
db.session.delete(module)
db.session.flush()
# Suppression des UEs
for ue in formation.ues:
sco_edit_ue.do_ue_delete(ue, force=True)
db.session.delete(formation)
db.session.delete(formation)
# news
ScolarNews.add(

View File

@ -33,7 +33,6 @@ import xml.dom.minidom
import flask
from flask import flash, g, url_for, request
from flask_login import current_user
from app.models.but_refcomp import ApcParcours
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -41,10 +40,13 @@ from app import db
from app import log
from app.models import Formation, Module
from app.models import ScolarNews
from app.models.but_refcomp import ApcParcours, ApcReferentielCompetences
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
@ -107,7 +109,7 @@ def formation_export(
in desired format
"""
formation: Formation = Formation.query.get_or_404(formation_id)
F = formation.to_dict()
F = formation.to_dict(with_refcomp_attrs=True)
selector = {"formation_id": formation_id}
if not export_external_ues:
selector["is_external"] = False
@ -174,7 +176,7 @@ def formation_export(
)
def formation_import_xml(doc: str, import_tags=True):
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' ))
XML may contain object (UE, modules) ids: this function returns two
@ -183,13 +185,12 @@ def formation_import_xml(doc: str, import_tags=True):
Args:
doc: str, xml data
import_tags: if false, does not import tags on modules.
use_local_refcomp: if True, utilise les id vers les ref. de compétences.
Returns:
formation_id, modules_old2new, ues_old2new
"""
from app.scodoc import sco_edit_formation
# log("formation_import_xml: doc=%s" % doc)
try:
dom = xml.dom.minidom.parseString(doc)
except Exception as exc:
@ -207,104 +208,130 @@ def formation_import_xml(doc: str, import_tags=True):
assert D[0] == "formation"
F = D[1]
F["dept_id"] = g.scodoc_dept_id
referentiel_competence_id = F.get("referentiel_competence_id")
# find new version number
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT max(version)
FROM notes_formations
WHERE acronyme=%(acronyme)s and titre=%(titre)s and dept_id=%(dept_id)s
""",
F,
# Pour les clonages, on prend le refcomp_id donné:
referentiel_competence_id = (
F.get("referentiel_competence_id") if use_local_refcomp else None
)
res = cursor.fetchall()
try:
version = int(res[0][0]) + 1
except (ValueError, IndexError, TypeError):
version = 1
F["version"] = version
# Sinon, on cherche a retrouver le ref. comp.
if referentiel_competence_id is None:
refcomp_version_orebut = F.get("refcomp_version_orebut")
refcomp_specialite = F.get("refcomp_specialite")
refcomp_type_titre = F.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:
referentiel_competence_id = refcomp.id
else:
flash(
f"Impossible de trouver le référentiel de compétence pour {refcomp_specialite} : est-il chargé ?"
)
F["referentiel_competence_id"] = referentiel_competence_id
# find new version number
formations = sco_formations.formation_list(
args={
"acronyme": F["acronyme"],
"titre": F["titre"],
"dept_id": F["dept_id"],
}
)
if formations:
version = max(f["version"] or 0 for f in formations)
else:
version = 0
F["version"] = version + 1
# create formation
# F_unquoted = F.copy()
# unescape_html_dict(F_unquoted)
formation_id = sco_edit_formation.do_formation_create(F)
log(f"formation {formation_id} created")
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)
ue_reference_to_id = {} # pour les coefs APC (map reference -> ue_id)
modules_a_coefficienter = [] # Liste des modules avec coefs APC
# -- create UEs
for ue_info in D[2]:
assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation_id
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
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
if xml_ue_id:
ues_old2new[xml_ue_id] = ue_id
# é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
# -- create matieres
for mat_info in ue_info[2]:
assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
# -- 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
mod_info[1]["formation_id"] = formation_id
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
mod_id = sco_edit_module.do_module_create(mod_info[1])
if xml_module_id:
modules_old2new[int(xml_module_id)] = mod_id
if len(mod_info) > 2:
module: Module = Module.query.get(mod_id)
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
elif child[0] == "parcours":
# associe les parcours de ce module (BUT)
code_parcours = child[1]["code"]
parcours = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcours:
module.parcours.append(parcours)
db.session.add(module)
else:
log(f"Warning: parcours {code_parcours} inexistant !")
if import_tags and tag_names:
sco_tag_module.module_tag_set(mod_id, tag_names)
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()
with sco_cache.DeferredSemCacheManager():
# -- create UEs
for ue_info in D[2]:
assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation_id
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
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
if xml_ue_id:
ues_old2new[xml_ue_id] = ue_id
# é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
# -- create matieres
for mat_info in ue_info[2]:
assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
# -- 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
mod_info[1]["formation_id"] = formation_id
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
mod_id = sco_edit_module.do_module_create(mod_info[1])
if xml_module_id:
modules_old2new[int(xml_module_id)] = mod_id
if len(mod_info) > 2:
module: Module = Module.query.get(mod_id)
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
elif child[0] == "parcours":
# Si on a un référentiel de compétences,
# associe les parcours de ce module (BUT)
if referentiel_competence_id:
code_parcours = child[1]["code"]
parcours = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcours:
module.parcours.append(parcours)
db.session.add(module)
else:
log(
f"Warning: parcours {code_parcours} inexistant !"
)
if import_tags and tag_names:
sco_tag_module.module_tag_set(mod_id, tag_names)
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()
return formation_id, modules_old2new, ues_old2new
@ -448,7 +475,9 @@ def formation_create_new_version(formation_id, redirect=True):
formation = Formation.query.get_or_404(formation_id)
resp = formation_export(formation_id, export_ids=True, format="xml")
xml_data = resp.get_data(as_text=True)
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
new_id, modules_old2new, ues_old2new = formation_import_xml(
xml_data, use_local_refcomp=True
)
# news
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,

View File

@ -1018,6 +1018,7 @@ a.discretelink:hover {
.help {
font-style: italic;
max-width: 800px;
}
.help_important {

View File

@ -652,14 +652,6 @@ def formation_export(formation_id, export_ids=False, format=None):
@scodoc7func
def formation_import_xml_form():
"form import d'une formation en XML"
H = [
html_sco_header.sco_header(page_title="Import d'une formation"),
"""<h2>Import d'une formation</h2>
<p>Création d'une formation (avec UE, matières, modules)
à partir un fichier XML (réservé aux utilisateurs avertis)</p>
""",
]
footer = html_sco_header.sco_footer()
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
@ -668,7 +660,15 @@ def formation_import_xml_form():
cancelbutton="Annuler",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + footer
return f"""
{ html_sco_header.sco_header(page_title="Import d'une formation") }
<h2>Import d'une formation</h2>
<p>Création d'une formation (avec UE, matières, modules)
à partir un fichier XML (réservé aux utilisateurs avertis)
</p>
{ tf[1] }
{ html_sco_header.sco_footer() }
"""
elif tf[0] == -1:
return flask.redirect(scu.NotesURL())
else:
@ -676,13 +676,14 @@ def formation_import_xml_form():
tf[2]["xmlfile"].read()
)
return (
"\n".join(H)
+ """<p>Import effectué !</p>
<p><a class="stdlink" href="ue_list?formation_id=%s">Voir la formation</a></p>"""
% formation_id
+ footer
)
return f"""
{ html_sco_header.sco_header(page_title="Import d'une formation") }
<h2>Import effectué !</h2>
<p><a class="stdlink" href="{
url_for("notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)}">Voir la formation</a></p>
{ html_sco_header.sco_footer() }
"""
sco_publish(