ScoDoc/app/views/refcomp.py

392 lines
13 KiB
Python

"""
PN / Référentiel de compétences
Emmanuel Viennet, 2021
"""
from pathlib import Path
import re
from flask import flash, url_for
from flask import current_app, g, request
from flask.templating import render_template
from flask_json import as_json
from flask_login import current_user
from markupsafe import Markup
from werkzeug.utils import redirect
from werkzeug.utils import secure_filename
from app import db, log
from app.decorators import scodoc, permission_required
from app.models import Formation
from app.models.but_refcomp import ApcReferentielCompetences
from app.but import change_refcomp
from app.but.import_refcomp import orebut_import_refcomp
from app.but.forms.refcomp_forms import (
FormationChangeRefCompForm,
FormationRefCompForm,
RefCompLoadForm,
RefCompUploadForm,
)
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import (
ScoFormatError,
ScoNoReferentielCompetences,
ScoValueError,
)
from app.scodoc.sco_permissions import Permission
from app.views import notes_bp as bp
from app.views import ScoData
@bp.route("/referentiel/comp/get/<int:refcomp_id>")
@scodoc
@permission_required(Permission.ScoView)
@as_json
def refcomp(refcomp_id):
"""Le référentiel de compétences, en JSON."""
ref: ApcReferentielCompetences = ApcReferentielCompetences.query.get_or_404(
refcomp_id
)
return ref.to_dict()
@bp.route("/referentiel/comp/show/<int:refcomp_id>")
@scodoc
@permission_required(Permission.ScoView)
def refcomp_show(refcomp_id):
"""Affichage du référentiel de compétences."""
referentiel_competence: ApcReferentielCompetences = (
ApcReferentielCompetences.query.get_or_404(refcomp_id)
)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = referentiel_competence.equivalents()
return render_template(
"but/refcomp_show.j2",
ref=referentiel_competence,
referentiels_equivalents=referentiels_equivalents,
title="Référentiel de compétences",
data_source=url_for(
"notes.refcomp",
scodoc_dept=g.scodoc_dept,
refcomp_id=refcomp_id,
),
sco=ScoData(),
table_niveaux_parcours=referentiel_competence.table_niveaux_parcours(),
)
@bp.route("/referentiel/comp/delete/<int:refcomp_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def refcomp_delete(refcomp_id):
"""Suppression du référentiel de la base. Le fichier source n'est pas affecté."""
ref = ApcReferentielCompetences.query.get_or_404(refcomp_id)
db.session.delete(ref)
db.session.commit()
flash("référentiel de compétences supprimé")
return redirect(url_for("notes.refcomp_table", scodoc_dept=g.scodoc_dept))
@bp.route("/referentiel/comp/table")
@scodoc
@permission_required(Permission.ScoView)
def refcomp_table():
"""Liste html des ref. comp. chargés dans ce département"""
refs = ApcReferentielCompetences.query.filter_by(dept_id=g.scodoc_dept_id)
columns_ids = ("type_titre", "specialite_long", "version", "json", "nb_formations")
if current_user.has_permission(Permission.EditFormation):
columns_ids = ("suppr",) + columns_ids
suppr_icon = scu.icontag(
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
)
tab = GenTable(
columns_ids=columns_ids,
titles={
"suppr": "",
"type_titre": "Type",
"specialite_long": "Spécialité",
"version": "Version",
"json": "Export",
"nb_formations": "Formations",
},
rows=[
{
"type_titre": ref.type_titre,
"specialite_long": ref.specialite_long,
"_specialite_long_target": url_for(
"notes.refcomp_show", scodoc_dept=g.scodoc_dept, refcomp_id=ref.id
),
"version": ref.get_version(),
"json": "json",
"_json_target": url_for(
"notes.refcomp", scodoc_dept=g.scodoc_dept, refcomp_id=ref.id
),
"nb_formations": len(ref.formations),
"suppr": suppr_icon,
"_suppr_target": url_for(
"notes.refcomp_delete", scodoc_dept=g.scodoc_dept, refcomp_id=ref.id
),
}
for ref in refs
],
table_id="refcomp_table",
)
return render_template(
"but/refcomp_table.j2",
tab=tab,
title="Référentiels de compétences",
sco=ScoData(),
)
@bp.route(
"/referentiel/comp/assoc_formation/<int:formation_id>", methods=["GET", "POST"]
)
@scodoc
@permission_required(Permission.EditFormation)
def refcomp_assoc_formation(formation_id: int):
"""Formulaire association ref. compétence"""
formation = Formation.query.get_or_404(formation_id)
form = FormationRefCompForm()
form.referentiel_competence.choices = [
(r.id, f"{r.type_titre} {r.specialite_long} ({r.get_version()})")
for r in ApcReferentielCompetences.query.filter_by(dept_id=g.scodoc_dept_id)
]
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
if form.validate_on_submit():
referentiel_competence_id = form.referentiel_competence.data
assert (
db.session.get(ApcReferentielCompetences, referentiel_competence_id)
is not None
)
if formation.referentiel_competence_id != referentiel_competence_id:
formation.refcomp_desassoc()
formation.referentiel_competence_id = referentiel_competence_id
db.session.add(formation)
db.session.commit()
flash("Nouveau référentiel de compétences associé")
else:
flash("Pas de changements")
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
return render_template(
"but/refcomp_assoc.j2",
form=form,
referentiel_competence_id=formation.referentiel_competence_id,
formation=formation,
title="Association réf. compétences",
)
@bp.route("/referentiel/comp/desassoc_formation/<int:formation_id>", methods=["GET"])
@scodoc
@permission_required(Permission.EditFormation)
def refcomp_desassoc_formation(formation_id: int):
"""Désassocie la formation de son ref. de compétence"""
formation: Formation = Formation.query.get_or_404(formation_id)
formation.refcomp_desassoc()
return redirect(
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id)
)
@bp.route(
"/referentiel/comp/load", defaults={"formation_id": None}, methods=["GET", "POST"]
)
@bp.route("/referentiel/comp/load/<int:formation_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def refcomp_load(formation_id=None):
"""Formulaire association ref. compétence"""
formation = (
Formation.get_formation(formation_id) if formation_id is not None else None
)
refs_distrib_files = sorted(
list(
(
Path(current_app.config["SCODOC_DIR"])
/ "ressources/referentiels/but2023/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-z0-9_]+)-([0-9]+)-([0-9]+).xml", str(filename))
if not m:
log(f"refcomp_load: ignoring {filename} (invalid filename)")
elif (
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} (already loaded)")
form = RefCompLoadForm()
if current_app and not current_app.config["DEBUG"]:
form.upload.render_kw = {"disabled": "disabled"}
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.referentiel_standard.data:
try:
filename = refs_distrib_dict[int(form.referentiel_standard.data)][
"filename"
]
except (ValueError, IndexError) as exc:
raise ScoValueError("choix invalide") from exc
else:
raise ScoValueError("choix invalide")
with open(filename, encoding="utf-8") as stream:
_upload_ref_xml(filename, stream)
return redirect(
url_for(
"notes.refcomp_table",
scodoc_dept=g.scodoc_dept,
)
)
return render_template(
"but/refcomp_load.j2",
form=form,
formation=formation,
title="Chargement réf. compétences",
)
def _upload_ref_xml(filename: str, stream):
"""
peut lever ScoFormatError.
"""
try:
xml_data = stream.read()
_ = orebut_import_refcomp(
xml_data, dept_id=g.scodoc_dept_id, orig_filename=Path(filename).name
)
except TypeError as exc:
raise ScoFormatError(f"fichier XML Orébut invalide (1): {exc.args}") from exc
#
flash(
Markup(f"Référentiel <tt>{Path(filename).name}</tt> chargé."),
category="info",
)
@bp.route(
"/referentiel/comp/upload_new",
defaults={"formation_id": None},
methods=["GET", "POST"],
)
@bp.route("/referentiel/comp/upload_new/<int:formation_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def refcomp_upload_new(formation_id=None):
"""Formulaire chargement fichier externe ref. compétence"""
formation = (
Formation.get_formation(formation_id) if formation_id is not None else None
)
form = RefCompUploadForm()
if form.validate_on_submit():
if form.upload.data:
f = form.upload.data
filename = secure_filename(f.filename)
_upload_ref_xml(filename, f)
return redirect(
url_for(
"notes.refcomp_table",
scodoc_dept=g.scodoc_dept,
)
)
if form.cancel.data: # cancel button
return redirect(
url_for(
"notes.refcomp_load",
scodoc_dept=g.scodoc_dept,
)
)
return render_template(
"but/refcomp_upload_new.j2",
form=form,
formation=formation,
title="Chargement réf. compétences externe",
)
@bp.route("/formation/<int:formation_id>/change_refcomp", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def formation_change_refcomp(formation_id: int):
"""Tente de changer le ref. de comp. de la formation"""
formation = Formation.get_formation(formation_id)
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
if ref_comp is None:
raise ScoNoReferentielCompetences(formation=formation)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = ref_comp.equivalents()
form = FormationChangeRefCompForm()
form.object_select.choices = [
(ref.id, ref.get_title()) for ref in referentiels_equivalents
]
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
)
if form.validate_on_submit():
try:
new_ref_id = int(form.object_select.data)
except TypeError as exc:
raise ScoValueError("nouveau refcomp id invalide") from exc
new_ref = None
for ref in referentiels_equivalents:
if ref.id == new_ref_id:
new_ref = ref
break
if new_ref is None:
raise ScoValueError("nouveau refcomp invalide")
change_refcomp.formation_change_referentiel(formation, new_ref)
flash("Formation changée de référentiel")
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
)
)
return render_template(
"but/change_refcomp.j2",
form=form,
formation=formation,
referentiels_equivalents=referentiels_equivalents,
title="Changer de référentiel de compétences",
)