diff --git a/app/models/formations.py b/app/models/formations.py index 1262729850..d42200cd30 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -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): diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 9167e7620d..4077f63355 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -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( diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index cdd8f959e9..5742d24832 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -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, diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index a5a133c684..95871981ed 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1018,6 +1018,7 @@ a.discretelink:hover { .help { font-style: italic; + max-width: 800px; } .help_important { diff --git a/app/views/notes.py b/app/views/notes.py index 46a4aae33e..b59150e6c2 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -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"), - """
Création d'une formation (avec UE, matières, modules) - à partir un fichier XML (réservé aux utilisateurs avertis)
- """, - ] - 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") } +Création d'une formation (avec UE, matières, modules) + à partir un fichier XML (réservé aux utilisateurs avertis) +
+ { 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) - + """Import effectué !
- """ - % formation_id - + footer - ) + return f""" + { html_sco_header.sco_header(page_title="Import d'une formation") } +