# -*- 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 # ############################################################################## """Import / Export de formations """ from operator import itemgetter 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 from app import db from app import log from app.models import Formation, Module from app.models import ScolarNews 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_formsemestre from app.scodoc import sco_preferences from app.scodoc import sco_tag_module from app.scodoc import sco_xml import sco_version from app.scodoc.gen_tables import GenTable from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_permissions import Permission _formationEditor = ndb.EditableTable( "notes_formations", "formation_id", ( "formation_id", "acronyme", "titre", "titre_officiel", "version", "formation_code", "type_parcours", "code_specialite", "referentiel_competence_id", ), filter_dept=True, sortkey="acronyme", ) def formation_list(formation_id=None, args={}): """List formation(s) with given id, or matching args (when args is given, formation_id is ignored). """ if not args: if formation_id is None: args = {} else: args = {"formation_id": formation_id} cnx = ndb.GetDBConnexion() r = _formationEditor.list(cnx, args=args) # log('%d formations found' % len(r)) return r def formation_has_locked_sems(formation_id): # XXX to remove "backward compat: True if there is a locked formsemestre in this formation" formation = Formation.query.get(formation_id) if formation is None: return False return formation.has_locked_sems() def formation_export( formation_id, export_ids=False, export_tags=True, export_external_ues=False, format=None, ): """Get a formation, with UE, matieres, modules in desired format """ formation: Formation = Formation.query.get_or_404(formation_id) F = formation.to_dict() selector = {"formation_id": formation_id} if not export_external_ues: selector["is_external"] = False ues = sco_edit_ue.ue_list(selector) F["ue"] = ues for ue in ues: ue_id = ue["ue_id"] ue["reference"] = ue_id # pour les coefficients if not export_ids: del ue["id"] del ue["ue_id"] del ue["formation_id"] if ue["ects"] is None: del ue["ects"] mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) ue["matiere"] = mats for mat in mats: matiere_id = mat["matiere_id"] if not export_ids: del mat["id"] del mat["matiere_id"] del mat["ue_id"] mods = sco_edit_module.module_list({"matiere_id": matiere_id}) mat["module"] = mods for mod in mods: module_id = mod["module_id"] if export_tags: tags = sco_tag_module.module_tag_list(module_id=mod["module_id"]) if tags: mod["tags"] = [{"name": x} for x in tags] # module = Module.query.get(module_id) if module.is_apc(): # Exporte les coefficients mod["coefficients"] = [ {"ue_reference": str(ue_id), "coef": str(coef)} for (ue_id, coef) in module.get_ue_coef_dict().items() ] # Et les parcours mod["parcours"] = [ p.to_dict(with_annees=False) for p in module.parcours ] if not export_ids: del mod["id"] del mod["ue_id"] del mod["matiere_id"] del mod["module_id"] del mod["formation_id"] if mod["ects"] is None: del mod["ects"] filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}" return scu.sendResult( F, name="formation", format=format, force_outer_xml_tag=False, attached=True, filename=filename, ) def formation_import_xml(doc: str, import_tags=True): """Create a formation from XML representation (format dumped by formation_export( format='xml' )) 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. 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: log("formation_import_xml: invalid XML data") raise ScoValueError("Fichier XML invalide") from exc try: f = dom.getElementsByTagName("formation")[0] # or dom.documentElement D = sco_xml.xml_to_dicts(f) except Exception as exc: raise ScoFormatError( """Ce document xml ne correspond pas à un programme exporté par ScoDoc. (élément 'formation' inexistant par exemple).""" ) from exc 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, ) res = cursor.fetchall() try: version = int(res[0][0]) + 1 except (ValueError, IndexError, TypeError): version = 1 F["version"] = version # 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("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 def formation_list_table(formation_id=None, args={}): """List formation, grouped by titre and sorted by versions and listing associated semestres returns a table """ formations = formation_list(formation_id=formation_id, args=args) title = "Programmes pédagogiques" lockicon = scu.icontag( "lock32_img", title="Comporte des semestres verrouillés", border="0" ) suppricon = scu.icontag( "delete_small_img", border="0", alt="supprimer", title="Supprimer" ) editicon = scu.icontag( "edit_img", border="0", alt="modifier", title="Modifier titres et code" ) editable = current_user.has_permission(Permission.ScoChangeFormation) # Traduit/ajoute des champs à afficher: for f in formations: try: f["parcours_name"] = sco_codes_parcours.get_parcours_from_code( f["type_parcours"] ).NAME except: f["parcours_name"] = "" f["_titre_target"] = url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=str(f["formation_id"]), ) f["_titre_link_class"] = "stdlink" f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-") # Ajoute les semestres associés à chaque formation: f["sems"] = sco_formsemestre.do_formsemestre_list( args={"formation_id": f["formation_id"]} ) f["sems_list_txt"] = ", ".join([s["session_id"] for s in f["sems"]]) f["_sems_list_txt_html"] = ", ".join( [ '%(' "session_id)s " % s for s in f["sems"] ] + ( [ 'ajouter ' % (f["acronyme"].lower().replace(" ", "-"), f["formation_id"]) ] if current_user.has_permission(Permission.ScoImplement) else [] ) ) if f["sems"]: f["date_fin_dernier_sem"] = max([s["date_fin_iso"] for s in f["sems"]]) f["annee_dernier_sem"] = f["date_fin_dernier_sem"].split("-")[0] else: f["date_fin_dernier_sem"] = "" f["annee_dernier_sem"] = "" locked = formation_has_locked_sems(f["formation_id"]) # if locked: but_locked = lockicon else: but_locked = '' if editable and not locked: but_suppr = ( '%s' % ( f["formation_id"], f["acronyme"].lower().replace(" ", "-"), suppricon, ) ) else: but_suppr = '' if editable: but_edit = ( '%s' % (f["formation_id"], f["acronyme"].lower().replace(" ", "-"), editicon) ) else: but_edit = '' f["buttons"] = "" f["_buttons_html"] = but_locked + but_suppr + but_edit # Tri par annee_denier_sem, type, acronyme, titre, version décroissante formations.sort(key=itemgetter("version"), reverse=True) formations.sort(key=itemgetter("titre")) formations.sort(key=itemgetter("acronyme")) formations.sort(key=itemgetter("parcours_name")) formations.sort( key=itemgetter("annee_dernier_sem"), reverse=True ) # plus recemments utilises en tete # columns_ids = ( "buttons", "acronyme", "parcours_name", "formation_code", "version", "titre", "sems_list_txt", ) titles = { "buttons": "", "acronyme": "Acro.", "parcours_name": "Type", "titre": "Titre", "version": "Version", "formation_code": "Code", "sems_list_txt": "Semestres", } return GenTable( columns_ids=columns_ids, rows=formations, titles=titles, origin="Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + "", caption=title, html_caption=title, table_id="formation_list_table", html_class="formation_list_table table_leftalign", html_with_td_classes=True, html_sortable=True, base_url="%s?formation_id=%s" % (request.base_url, formation_id), page_title=title, pdf_title=title, preferences=sco_preferences.SemPreferences(), ) def formation_create_new_version(formation_id, redirect=True): "duplicate formation, with new version number" 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) # news ScolarNews.add( typ=ScolarNews.NEWS_FORM, obj=new_id, text=f"Nouvelle version de la formation {formation.acronyme}", ) if redirect: flash("Nouvelle version !") return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=new_id, msg="Nouvelle version !", ) ) else: return new_id, modules_old2new, ues_old2new