forked from ScoDoc/ScoDoc
387 lines
13 KiB
Python
387 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.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.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.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.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.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()
|
|
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",
|
|
)
|