From efe1b1d734cb5c79a331d6285efd2365aca16571 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 12 Feb 2022 20:25:22 +0100 Subject: [PATCH] Chargement et association des ref. comp. BUT --- app/but/forms/refcomp_forms.py | 16 +++- app/but/import_refcomp.py | 25 ++++- app/models/but_refcomp.py | 9 +- app/templates/but/refcomp_load.html | 15 ++- app/views/refcomp.py | 69 ++++++++++++-- .../versions/bd2c1c3d866e_refcomp_orebut.py | 94 +++++++++++++++++++ 6 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 migrations/versions/bd2c1c3d866e_refcomp_orebut.py diff --git a/app/but/forms/refcomp_forms.py b/app/but/forms/refcomp_forms.py index 6fcd59500b..ce16292dfd 100644 --- a/app/but/forms/refcomp_forms.py +++ b/app/but/forms/refcomp_forms.py @@ -19,10 +19,12 @@ class FormationRefCompForm(FlaskForm): class RefCompLoadForm(FlaskForm): + referentiel_standard = SelectField( + "Choisir un référentiel de compétences officiel BUT" + ) upload = FileField( - label="Sélectionner un fichier XML Orébut", + label="Ou bien sélectionner un fichier XML au format Orébut", validators=[ - FileRequired(), FileAllowed( [ "xml", @@ -33,3 +35,13 @@ class RefCompLoadForm(FlaskForm): ) submit = SubmitField("Valider") cancel = SubmitField("Annuler") + + def validate(self): + if not super().validate(): + return False + if (self.referentiel_standard.data == "0") == (not self.upload.data): + self.referentiel_standard.errors.append( + "Choisir soit un référentiel, soit un fichier xml" + ) + return False + return True diff --git a/app/but/import_refcomp.py b/app/but/import_refcomp.py index 4f055147bc..0f97cd958c 100644 --- a/app/but/import_refcomp.py +++ b/app/but/import_refcomp.py @@ -6,6 +6,8 @@ from xml.etree import ElementTree from typing import TextIO +import sqlalchemy + from app import db from app.models.but_refcomp import ( @@ -19,7 +21,7 @@ from app.models.but_refcomp import ( ApcAnneeParcours, ApcParcoursNiveauCompetence, ) -from app.scodoc.sco_exceptions import ScoFormatError +from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): @@ -27,6 +29,16 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): peut lever TypeError ou ScoFormatError Résultat: instance de ApcReferentielCompetences """ + # Vérifie que le même fichier n'a pas déjà été chargé: + if ApcReferentielCompetences.query.filter_by( + scodoc_orig_filename=orig_filename, dept_id=dept_id + ).count(): + raise ScoValueError( + f"""Un référentiel a déjà été chargé d'un fichier de même nom. + ({orig_filename}) + Supprimez-le ou changer le nom du fichier.""" + ) + try: root = ElementTree.XML(xml_data) except ElementTree.ParseError as exc: @@ -42,7 +54,16 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None): if not competences: raise ScoFormatError("élément 'competences' manquant") for competence in competences.findall("competence"): - c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib)) + try: + c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib)) + db.session.flush() + except sqlalchemy.exc.IntegrityError: + # ne devrait plus se produire car pas d'unicité de l'id: donc inutile + db.session.rollback() + raise ScoValueError( + f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]}) + """ + ) ref.competences.append(c) # --- SITUATIONS situations = competence.find("situations") diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 7981642539..429c63898e 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -81,6 +81,9 @@ class ApcReferentielCompetences(db.Model, XMLModel): ) formations = db.relationship("Formation", backref="referentiel_competence") + def __repr__(self): + return f"" + def to_dict(self): """Représentation complète du ref. de comp. comme un dict. @@ -110,7 +113,8 @@ class ApcCompetence(db.Model, XMLModel): db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False ) # les compétences dans Orébut sont identifiées par leur id unique - id_orebut = db.Column(db.Text(), nullable=True, index=True, unique=True) + # (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts) + id_orebut = db.Column(db.Text(), nullable=True, index=True) titre = db.Column(db.Text(), nullable=False, index=True) titre_long = db.Column(db.Text()) couleur = db.Column(db.Text()) @@ -139,6 +143,9 @@ class ApcCompetence(db.Model, XMLModel): cascade="all, delete-orphan", ) + def __repr__(self): + return f"" + def to_dict(self): return { "id_orebut": self.id_orebut, diff --git a/app/templates/but/refcomp_load.html b/app/templates/but/refcomp_load.html index 9bf70470b7..290de937b2 100644 --- a/app/templates/but/refcomp_load.html +++ b/app/templates/but/refcomp_load.html @@ -6,10 +6,23 @@

Charger un référentiel de compétences

-
+
{{ wtf.quick_form(form) }}
+ {% endblock %} \ No newline at end of file diff --git a/app/views/refcomp.py b/app/views/refcomp.py index 893f65c8de..27e5c83e22 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -3,9 +3,11 @@ PN / Référentiel de compétences Emmanuel Viennet, 2021 """ +from pathlib import Path +import re -from flask import url_for, flash -from flask import jsonify +from flask import jsonify, flash, url_for +from flask import Markup from flask import current_app, g, request from flask.templating import render_template from flask_login import current_user @@ -15,7 +17,7 @@ from werkzeug.utils import secure_filename from config import Config from app import db -from app import models +from app import log from app.decorators import scodoc, permission_required from app.models import Formation @@ -23,9 +25,8 @@ from app.models.but_refcomp import ApcReferentielCompetences from app.but.import_refcomp import orebut_import_refcomp from app.but.forms.refcomp_forms import FormationRefCompForm, RefCompLoadForm from app.scodoc.gen_tables import GenTable -from app.scodoc import html_sidebar from app.scodoc import sco_utils as scu -from app.scodoc.sco_exceptions import ScoFormatError +from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError from app.scodoc.sco_permissions import Permission from app.views import notes_bp as bp from app.views import ScoData @@ -171,14 +172,61 @@ def refcomp_load(formation_id=None): formation = Formation.query.get_or_404(formation_id) else: formation = None + refs_distrib_files = sorted( + list( + ( + Path(current_app.config["SCODOC_DIR"]) + / "ressources/referentiels/but2022/competences" + ).glob("*.xml") + ) + ) + refs_distrib_dict = [{"id": 0, "specialite": "Aucun", "created": "", "serial": ""}] + i = 1 + for filename in refs_distrib_files: + m = re.match(r".*/but-([A-Za-z_]+)-([0-9]+)-([0-9]+).xml", str(filename)) + if ( + m + and ApcReferentielCompetences.query.filter_by( + scodoc_orig_filename=Path(filename).name, dept_id=g.scodoc_dept_id + ).count() + == 0 + ): + refs_distrib_dict.append( + { + "id": i, + "specialite": m.group(1), + "created": m.group(2), + "serial": m.group(3), + "filename": str(filename), + } + ) + i += 1 + else: + log(f"refcomp_load: ignoring {filename} (invalid filename)") + form = RefCompLoadForm() + form.referentiel_standard.choices = [ + (r["id"], f"{r['specialite']} ({r['created']}-{r['serial']})") + for r in refs_distrib_dict + ] if form.validate_on_submit(): - f = form.upload.data - filename = secure_filename(f.filename) + if form.upload.data: + f = form.upload.data + filename = secure_filename(f.filename) + elif form.referentiel_standard.data: + try: + filename = refs_distrib_dict[int(form.referentiel_standard.data)][ + "filename" + ] + except (ValueError, IndexError): + raise ScoValueError("choix invalide") + f = open(filename) + else: + raise ScoValueError("choix invalide") try: xml_data = f.read() _ = orebut_import_refcomp( - xml_data, dept_id=g.scodoc_dept_id, orig_filename=filename + xml_data, dept_id=g.scodoc_dept_id, orig_filename=Path(filename).name ) except TypeError as exc: raise ScoFormatError( @@ -187,6 +235,11 @@ def refcomp_load(formation_id=None): except ScoFormatError: raise + flash( + Markup(f"Référentiel {Path(filename).name} chargé."), + category="info", + ) + if formation is not None: return redirect( url_for( diff --git a/migrations/versions/bd2c1c3d866e_refcomp_orebut.py b/migrations/versions/bd2c1c3d866e_refcomp_orebut.py new file mode 100644 index 0000000000..8475cd95e6 --- /dev/null +++ b/migrations/versions/bd2c1c3d866e_refcomp_orebut.py @@ -0,0 +1,94 @@ +"""refcomp orebut + +Revision ID: bd2c1c3d866e +Revises: c95d5a3bf0de +Create Date: 2022-02-12 15:17:42.298699 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "bd2c1c3d866e" +down_revision = "c95d5a3bf0de" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + op.add_column("apc_competence", sa.Column("id_orebut", sa.Text(), nullable=True)) + op.drop_constraint( + "apc_competence_referentiel_id_titre_key", "apc_competence", type_="unique" + ) + op.create_index( + op.f("ix_apc_competence_id_orebut"), + "apc_competence", + ["id_orebut"], + ) + op.add_column( + "apc_referentiel_competences", sa.Column("annexe", sa.Text(), nullable=True) + ) + op.add_column( + "apc_referentiel_competences", + sa.Column("type_structure", sa.Text(), nullable=True), + ) + op.add_column( + "apc_referentiel_competences", + sa.Column("type_departement", sa.Text(), nullable=True), + ) + op.add_column( + "apc_referentiel_competences", + sa.Column("version_orebut", sa.Text(), nullable=True), + ) + + op.create_index( + op.f("ix_notes_formsemestre_uecoef_formsemestre_id"), + "notes_formsemestre_uecoef", + ["formsemestre_id"], + unique=False, + ) + op.create_index( + op.f("ix_notes_formsemestre_uecoef_ue_id"), + "notes_formsemestre_uecoef", + ["ue_id"], + unique=False, + ) + op.create_index( + op.f("ix_scolar_formsemestre_validation_is_external"), + "scolar_formsemestre_validation", + ["is_external"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("ix_scolar_formsemestre_validation_is_external"), + table_name="scolar_formsemestre_validation", + ) + op.drop_index( + op.f("ix_notes_formsemestre_uecoef_ue_id"), + table_name="notes_formsemestre_uecoef", + ) + op.drop_index( + op.f("ix_notes_formsemestre_uecoef_formsemestre_id"), + table_name="notes_formsemestre_uecoef", + ) + + op.drop_column("apc_referentiel_competences", "version_orebut") + op.drop_column("apc_referentiel_competences", "type_departement") + op.drop_column("apc_referentiel_competences", "type_structure") + op.drop_column("apc_referentiel_competences", "annexe") + op.drop_index(op.f("ix_apc_competence_id_orebut"), table_name="apc_competence") + op.create_unique_constraint( + "apc_competence_referentiel_id_titre_key", + "apc_competence", + ["referentiel_id", "titre"], + ) + op.drop_column("apc_competence", "id_orebut") + # ### end Alembic commands ###