Chargement et association des ref. comp. BUT

This commit is contained in:
Emmanuel Viennet 2022-02-12 20:25:22 +01:00
parent 2610d746b9
commit efe1b1d734
6 changed files with 214 additions and 14 deletions

View File

@ -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

View File

@ -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"):
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")

View File

@ -81,6 +81,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
)
formations = db.relationship("Formation", backref="referentiel_competence")
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite}>"
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"<ApcCompetence {self.id} {self.titre}>"
def to_dict(self):
return {
"id_orebut": self.id_orebut,

View File

@ -6,10 +6,23 @@
<h1>Charger un référentiel de compétences</h1>
<div class="row">
<div class="col-md-4">
<div class="col-md-5">
{{ wtf.quick_form(form) }}
</div>
</div>
<div class="row">
<div class="col-md-5">
<ul>
<li>
<a href="{{ url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept, ) }}">
Liste des référentiels de compétences chargés</a>
</li>
<li>
<a href="{{ url_for('notes.refcomp_assoc_formation', scodoc_dept=g.scodoc_dept, formation_id=formation.id) }}">
Association à la formation {{ formation.acronyme }}</a>
</li>
</div>
</div>
{% endblock %}

View File

@ -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():
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 <tt>{Path(filename).name}</tt> chargé."),
category="info",
)
if formation is not None:
return redirect(
url_for(

View File

@ -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 ###