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" "titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}""" 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 = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
e["departement"] = self.departement.to_dict() e["departement"] = self.departement.to_dict()
# ScoDoc7 output_formators: (backward compat) e["formation_id"] = self.id # ScoDoc7 backward compat
e["formation_id"] = self.id 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 return e
def get_parcours(self): 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.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject
from app.scodoc import html_sco_header 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_codes_parcours
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -117,15 +118,16 @@ def do_formation_delete(formation_id):
), ),
) )
# Suppression des modules with sco_cache.DeferredSemCacheManager():
for module in formation.modules: # Suppression des modules
db.session.delete(module) for module in formation.modules:
db.session.flush() db.session.delete(module)
# Suppression des UEs db.session.flush()
for ue in formation.ues: # Suppression des UEs
sco_edit_ue.do_ue_delete(ue, force=True) for ue in formation.ues:
sco_edit_ue.do_ue_delete(ue, force=True)
db.session.delete(formation) db.session.delete(formation)
# news # news
ScolarNews.add( ScolarNews.add(

View File

@ -33,7 +33,6 @@ import xml.dom.minidom
import flask import flask
from flask import flash, g, url_for, request from flask import flash, g, url_for, request
from flask_login import current_user from flask_login import current_user
from app.models.but_refcomp import ApcParcours
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -41,10 +40,13 @@ from app import db
from app import log from app import log
from app.models import Formation, Module from app.models import Formation, Module
from app.models import ScolarNews 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_codes_parcours
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
@ -107,7 +109,7 @@ def formation_export(
in desired format in desired format
""" """
formation: Formation = Formation.query.get_or_404(formation_id) 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} selector = {"formation_id": formation_id}
if not export_external_ues: if not export_external_ues:
selector["is_external"] = False 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 """Create a formation from XML representation
(format dumped by formation_export( format='xml' )) (format dumped by formation_export( format='xml' ))
XML may contain object (UE, modules) ids: this function returns two 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: Args:
doc: str, xml data doc: str, xml data
import_tags: if false, does not import tags on modules. 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: Returns:
formation_id, modules_old2new, ues_old2new formation_id, modules_old2new, ues_old2new
""" """
from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_formation
# log("formation_import_xml: doc=%s" % doc)
try: try:
dom = xml.dom.minidom.parseString(doc) dom = xml.dom.minidom.parseString(doc)
except Exception as exc: except Exception as exc:
@ -207,104 +208,130 @@ def formation_import_xml(doc: str, import_tags=True):
assert D[0] == "formation" assert D[0] == "formation"
F = D[1] F = D[1]
F["dept_id"] = g.scodoc_dept_id F["dept_id"] = g.scodoc_dept_id
referentiel_competence_id = F.get("referentiel_competence_id") # Pour les clonages, on prend le refcomp_id donné:
# find new version number referentiel_competence_id = (
cnx = ndb.GetDBConnexion() F.get("referentiel_competence_id") if use_local_refcomp else None
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,
) )
res = cursor.fetchall() # Sinon, on cherche a retrouver le ref. comp.
try: if referentiel_competence_id is None:
version = int(res[0][0]) + 1 refcomp_version_orebut = F.get("refcomp_version_orebut")
except (ValueError, IndexError, TypeError): refcomp_specialite = F.get("refcomp_specialite")
version = 1 refcomp_type_titre = F.get("refcomp_type_titre")
F["version"] = version 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 # create formation
# F_unquoted = F.copy()
# unescape_html_dict(F_unquoted)
formation_id = sco_edit_formation.do_formation_create(F) formation_id = sco_edit_formation.do_formation_create(F)
log(f"formation {formation_id} created") log(f"formation {formation_id} created")
ues_old2new = {} # xml ue_id : new ue_id ues_old2new = {} # xml ue_id : new ue_id
modules_old2new = {} # xml module_id : new module_id modules_old2new = {} # xml module_id : new module_id
# (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML) # (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML)
ue_reference_to_id = {} # pour les coefs APC (map reference -> ue_id) ue_reference_to_id = {} # pour les coefs APC (map reference -> ue_id)
modules_a_coefficienter = [] # Liste des modules avec coefs APC modules_a_coefficienter = [] # Liste des modules avec coefs APC
# -- create UEs with sco_cache.DeferredSemCacheManager():
for ue_info in D[2]: # -- create UEs
assert ue_info[0] == "ue" for ue_info in D[2]:
ue_info[1]["formation_id"] = formation_id assert ue_info[0] == "ue"
if "ue_id" in ue_info[1]: ue_info[1]["formation_id"] = formation_id
xml_ue_id = int(ue_info[1]["ue_id"]) if "ue_id" in ue_info[1]:
del ue_info[1]["ue_id"] xml_ue_id = int(ue_info[1]["ue_id"])
else: del ue_info[1]["ue_id"]
xml_ue_id = None else:
ue_id = sco_edit_ue.do_ue_create(ue_info[1]) xml_ue_id = None
if xml_ue_id: ue_id = sco_edit_ue.do_ue_create(ue_info[1])
ues_old2new[xml_ue_id] = ue_id if xml_ue_id:
# élément optionnel présent dans les exports BUT: ues_old2new[xml_ue_id] = ue_id
ue_reference = ue_info[1].get("reference") # élément optionnel présent dans les exports BUT:
if ue_reference: ue_reference = ue_info[1].get("reference")
ue_reference_to_id[int(ue_reference)] = ue_id if ue_reference:
# -- create matieres ue_reference_to_id[int(ue_reference)] = ue_id
for mat_info in ue_info[2]: # -- create matieres
assert mat_info[0] == "matiere" for mat_info in ue_info[2]:
mat_info[1]["ue_id"] = ue_id assert mat_info[0] == "matiere"
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1]) mat_info[1]["ue_id"] = ue_id
# -- create modules mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
for mod_info in mat_info[2]: # -- create modules
assert mod_info[0] == "module" for mod_info in mat_info[2]:
if "module_id" in mod_info[1]: assert mod_info[0] == "module"
xml_module_id = int(mod_info[1]["module_id"]) if "module_id" in mod_info[1]:
del mod_info[1]["module_id"] xml_module_id = int(mod_info[1]["module_id"])
else: del mod_info[1]["module_id"]
xml_module_id = None else:
mod_info[1]["formation_id"] = formation_id xml_module_id = None
mod_info[1]["matiere_id"] = mat_id mod_info[1]["formation_id"] = formation_id
mod_info[1]["ue_id"] = ue_id mod_info[1]["matiere_id"] = mat_id
if not "module_type" in mod_info[1]: mod_info[1]["ue_id"] = ue_id
mod_info[1]["module_type"] = scu.ModuleType.STANDARD if not "module_type" in mod_info[1]:
mod_id = sco_edit_module.do_module_create(mod_info[1]) mod_info[1]["module_type"] = scu.ModuleType.STANDARD
if xml_module_id: mod_id = sco_edit_module.do_module_create(mod_info[1])
modules_old2new[int(xml_module_id)] = mod_id if xml_module_id:
if len(mod_info) > 2: modules_old2new[int(xml_module_id)] = mod_id
module: Module = Module.query.get(mod_id) if len(mod_info) > 2:
tag_names = [] module: Module = Module.query.get(mod_id)
ue_coef_dict = {} tag_names = []
for child in mod_info[2]: ue_coef_dict = {}
if child[0] == "tags" and import_tags: for child in mod_info[2]:
tag_names.append(child[1]["name"]) if child[0] == "tags" and import_tags:
elif child[0] == "coefficients": tag_names.append(child[1]["name"])
ue_reference = int(child[1]["ue_reference"]) elif child[0] == "coefficients":
coef = float(child[1]["coef"]) ue_reference = int(child[1]["ue_reference"])
ue_coef_dict[ue_reference] = coef coef = float(child[1]["coef"])
elif child[0] == "parcours": ue_coef_dict[ue_reference] = coef
# associe les parcours de ce module (BUT) elif child[0] == "parcours":
code_parcours = child[1]["code"] # Si on a un référentiel de compétences,
parcours = ApcParcours.query.filter_by( # associe les parcours de ce module (BUT)
code=code_parcours, if referentiel_competence_id:
referentiel_id=referentiel_competence_id, code_parcours = child[1]["code"]
).first() parcours = ApcParcours.query.filter_by(
if parcours: code=code_parcours,
module.parcours.append(parcours) referentiel_id=referentiel_competence_id,
db.session.add(module) ).first()
else: if parcours:
log(f"Warning: parcours {code_parcours} inexistant !") module.parcours.append(parcours)
if import_tags and tag_names: db.session.add(module)
sco_tag_module.module_tag_set(mod_id, tag_names) else:
if module.is_apc() and ue_coef_dict: log(
modules_a_coefficienter.append((module, ue_coef_dict)) f"Warning: parcours {code_parcours} inexistant !"
# Fixe les coefs APC (à la fin pour que les UE soient crées) )
for module, ue_coef_dict_ref in modules_a_coefficienter: if import_tags and tag_names:
# remap ue ids: sco_tag_module.module_tag_set(mod_id, tag_names)
ue_coef_dict = {ue_reference_to_id[k]: v for (k, v) in ue_coef_dict_ref.items()} if module.is_apc() and ue_coef_dict:
module.set_ue_coef_dict(ue_coef_dict) modules_a_coefficienter.append((module, ue_coef_dict))
db.session.commit() # 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 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) formation = Formation.query.get_or_404(formation_id)
resp = formation_export(formation_id, export_ids=True, format="xml") resp = formation_export(formation_id, export_ids=True, format="xml")
xml_data = resp.get_data(as_text=True) 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 # news
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,

View File

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

View File

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