forked from ScoDoc/ScoDoc
Associer une formation BUT à un nouveau référentiel 'équivalent'.
This commit is contained in:
parent
3f6e65b9da
commit
5a5ddcacd7
@ -17,53 +17,11 @@ from app.models import (
|
|||||||
ApcValidationRCUE,
|
ApcValidationRCUE,
|
||||||
Formation,
|
Formation,
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
Module,
|
|
||||||
UniteEns,
|
UniteEns,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def map_referentiels(
|
|
||||||
ref1: ApcReferentielCompetences, ref2: ApcReferentielCompetences
|
|
||||||
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
|
|
||||||
"""Build mapping between two referentiels"""
|
|
||||||
if ref1.type_structure != ref2.type_structure:
|
|
||||||
return "type_structure mismatch"
|
|
||||||
if ref1.type_departement != ref2.type_departement:
|
|
||||||
return "type_departement mismatch"
|
|
||||||
# mêmes parcours ?
|
|
||||||
parcours_by_code_1 = {p.code: p for p in ref1.parcours}
|
|
||||||
parcours_by_code_2 = {p.code: p for p in ref2.parcours}
|
|
||||||
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
|
|
||||||
return "parcours mismatch"
|
|
||||||
parcours_map = {
|
|
||||||
parcours_by_code_1[code].id: parcours_by_code_2[code].id
|
|
||||||
for code in parcours_by_code_1
|
|
||||||
}
|
|
||||||
# mêmes compétences ?
|
|
||||||
competence_by_code_1 = {c.titre: c for c in ref1.competences}
|
|
||||||
competence_by_code_2 = {c.titre: c for c in ref2.competences}
|
|
||||||
if competence_by_code_1.keys() != competence_by_code_2.keys():
|
|
||||||
return "competences mismatch"
|
|
||||||
competences_map = {
|
|
||||||
competence_by_code_1[titre].id: competence_by_code_2[titre].id
|
|
||||||
for titre in competence_by_code_1
|
|
||||||
}
|
|
||||||
# mêmes niveaux (dans chaque compétence) ?
|
|
||||||
niveaux_map = {}
|
|
||||||
for titre in competence_by_code_1:
|
|
||||||
c1 = competence_by_code_1[titre]
|
|
||||||
c2 = competence_by_code_2[titre]
|
|
||||||
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
|
|
||||||
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
|
|
||||||
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
|
|
||||||
return f"niveaux mismatch in comp. '{titre}'"
|
|
||||||
niveaux_map.update(
|
|
||||||
{niveau_by_attr_1[a].id: niveau_by_attr_2[a].id for a in niveau_by_attr_1}
|
|
||||||
)
|
|
||||||
return parcours_map, competences_map, niveaux_map
|
|
||||||
|
|
||||||
|
|
||||||
def formation_change_referentiel(
|
def formation_change_referentiel(
|
||||||
formation: Formation, new_ref: ApcReferentielCompetences
|
formation: Formation, new_ref: ApcReferentielCompetences
|
||||||
):
|
):
|
||||||
@ -73,7 +31,7 @@ def formation_change_referentiel(
|
|||||||
if not isinstance(new_ref, ApcReferentielCompetences):
|
if not isinstance(new_ref, ApcReferentielCompetences):
|
||||||
raise ScoValueError("nouveau référentiel invalide")
|
raise ScoValueError("nouveau référentiel invalide")
|
||||||
|
|
||||||
r = map_referentiels(formation.referentiel_competence, new_ref)
|
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
|
||||||
if isinstance(r, str):
|
if isinstance(r, str):
|
||||||
raise ScoValueError(f"référentiels incompatibles: {r}")
|
raise ScoValueError(f"référentiels incompatibles: {r}")
|
||||||
parcours_map, competences_map, niveaux_map = r
|
parcours_map, competences_map, niveaux_map = r
|
||||||
|
@ -10,9 +10,11 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from wtforms import SelectField, SubmitField
|
from wtforms import SelectField, SubmitField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
|
||||||
class FormationRefCompForm(FlaskForm):
|
class FormationRefCompForm(FlaskForm):
|
||||||
|
"Choix d'un référentiel"
|
||||||
referentiel_competence = SelectField(
|
referentiel_competence = SelectField(
|
||||||
"Choisir parmi les référentiels déjà chargés :"
|
"Choisir parmi les référentiels déjà chargés :"
|
||||||
)
|
)
|
||||||
@ -21,6 +23,7 @@ class FormationRefCompForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class RefCompLoadForm(FlaskForm):
|
class RefCompLoadForm(FlaskForm):
|
||||||
|
"Upload d'un référentiel"
|
||||||
referentiel_standard = SelectField(
|
referentiel_standard = SelectField(
|
||||||
"Choisir un référentiel de compétences officiel BUT"
|
"Choisir un référentiel de compétences officiel BUT"
|
||||||
)
|
)
|
||||||
@ -47,3 +50,12 @@ class RefCompLoadForm(FlaskForm):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FormationChangeRefCompForm(FlaskForm):
|
||||||
|
"choix d'un nouveau ref. comp. pour une formation"
|
||||||
|
object_select = SelectField(
|
||||||
|
"Choisir le nouveau référentiel", validators=[DataRequired()]
|
||||||
|
)
|
||||||
|
submit = SubmitField("Changer le référentiel de la formation")
|
||||||
|
cancel = SubmitField("Annuler")
|
||||||
|
@ -8,16 +8,19 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
import yaml
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from sqlalchemy.orm import class_mapper
|
from sqlalchemy.orm import class_mapper
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db, log
|
||||||
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||||
|
|
||||||
|
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
|
||||||
|
|
||||||
|
|
||||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||||
@ -104,6 +107,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||||
|
|
||||||
|
def get_title(self) -> str:
|
||||||
|
"Titre affichable"
|
||||||
|
# utilise type_titre (B.U.T.), spécialité, version
|
||||||
|
return f"{self.type_titre} {self.specialite} {self.get_version()}"
|
||||||
|
|
||||||
def get_version(self) -> str:
|
def get_version(self) -> str:
|
||||||
"La version, normalement sous forme de date iso yyy-mm-dd"
|
"La version, normalement sous forme de date iso yyy-mm-dd"
|
||||||
if not self.version_orebut:
|
if not self.version_orebut:
|
||||||
@ -124,9 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
"type_departement": self.type_departement,
|
"type_departement": self.type_departement,
|
||||||
"type_titre": self.type_titre,
|
"type_titre": self.type_titre,
|
||||||
"version_orebut": self.version_orebut,
|
"version_orebut": self.version_orebut,
|
||||||
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
|
"scodoc_date_loaded": (
|
||||||
|
self.scodoc_date_loaded.isoformat() + "Z"
|
||||||
if self.scodoc_date_loaded
|
if self.scodoc_date_loaded
|
||||||
else "",
|
else ""
|
||||||
|
),
|
||||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||||
"competences": {
|
"competences": {
|
||||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||||
@ -234,6 +244,92 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
|
|
||||||
return parcours_info
|
return parcours_info
|
||||||
|
|
||||||
|
def equivalents(self) -> set["ApcReferentielCompetences"]:
|
||||||
|
"""Ensemble des référentiels du même département
|
||||||
|
qui peuvent être considérés comme "équivalents", au sens
|
||||||
|
une formation de ce référentiel pourrait changer vers un équivalent,
|
||||||
|
en ignorant les apprentissages critiques.
|
||||||
|
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
|
||||||
|
niveaux et parcours (voir map_to_other_referentiel).
|
||||||
|
"""
|
||||||
|
candidats = ApcReferentielCompetences.query.filter_by(
|
||||||
|
dept_id=self.dept_id
|
||||||
|
).filter(ApcReferentielCompetences.id != self.id)
|
||||||
|
return {
|
||||||
|
referentiel
|
||||||
|
for referentiel in candidats
|
||||||
|
if not isinstance(self.map_to_other_referentiel(referentiel), str)
|
||||||
|
}
|
||||||
|
|
||||||
|
def map_to_other_referentiel(
|
||||||
|
self, other: "ApcReferentielCompetences"
|
||||||
|
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
|
||||||
|
"""Build mapping between this referentiel and ref2.
|
||||||
|
If successful, returns 3 dicts mapping self ids to other ids.
|
||||||
|
Else return a string, error message.
|
||||||
|
"""
|
||||||
|
if self.type_structure != other.type_structure:
|
||||||
|
return "type_structure mismatch"
|
||||||
|
if self.type_departement != other.type_departement:
|
||||||
|
return "type_departement mismatch"
|
||||||
|
# Table d'équivalences entre refs:
|
||||||
|
equiv = self._load_config_equivalences()
|
||||||
|
# mêmes parcours ?
|
||||||
|
eq_parcours = equiv.get("parcours", {})
|
||||||
|
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
||||||
|
parcours_by_code_2 = {
|
||||||
|
eq_parcours.get(p.code, p.code): p for p in other.parcours
|
||||||
|
}
|
||||||
|
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
|
||||||
|
return "parcours mismatch"
|
||||||
|
parcours_map = {
|
||||||
|
parcours_by_code_1[eq_parcours.get(code, code)]
|
||||||
|
.id: parcours_by_code_2[eq_parcours.get(code, code)]
|
||||||
|
.id
|
||||||
|
for code in parcours_by_code_1
|
||||||
|
}
|
||||||
|
# mêmes compétences ?
|
||||||
|
competence_by_code_1 = {c.titre: c for c in self.competences}
|
||||||
|
competence_by_code_2 = {c.titre: c for c in other.competences}
|
||||||
|
if competence_by_code_1.keys() != competence_by_code_2.keys():
|
||||||
|
return "competences mismatch"
|
||||||
|
competences_map = {
|
||||||
|
competence_by_code_1[titre].id: competence_by_code_2[titre].id
|
||||||
|
for titre in competence_by_code_1
|
||||||
|
}
|
||||||
|
# mêmes niveaux (dans chaque compétence) ?
|
||||||
|
niveaux_map = {}
|
||||||
|
for titre in competence_by_code_1:
|
||||||
|
c1 = competence_by_code_1[titre]
|
||||||
|
c2 = competence_by_code_2[titre]
|
||||||
|
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
|
||||||
|
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
|
||||||
|
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
|
||||||
|
return f"niveaux mismatch in comp. '{titre}'"
|
||||||
|
niveaux_map.update(
|
||||||
|
{
|
||||||
|
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
|
||||||
|
for a in niveau_by_attr_1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return parcours_map, competences_map, niveaux_map
|
||||||
|
|
||||||
|
def _load_config_equivalences(self) -> dict:
|
||||||
|
"""Load config file ressources/referentiels/equivalences.yaml
|
||||||
|
used to define equivalences between distinct referentiels
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
||||||
|
doc = yaml.safe_load(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
|
||||||
|
return {}
|
||||||
|
except yaml.parser.ParserError as exc:
|
||||||
|
raise ScoValueError(
|
||||||
|
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
|
||||||
|
) from exc
|
||||||
|
return doc.get(self.specialite, {})
|
||||||
|
|
||||||
|
|
||||||
class ApcCompetence(db.Model, XMLModel):
|
class ApcCompetence(db.Model, XMLModel):
|
||||||
"Compétence"
|
"Compétence"
|
||||||
@ -374,9 +470,11 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
"libelle": self.libelle,
|
"libelle": self.libelle,
|
||||||
"annee": self.annee,
|
"annee": self.annee,
|
||||||
"ordre": self.ordre,
|
"ordre": self.ordre,
|
||||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
"app_critiques": (
|
||||||
|
{x.code: x.to_dict() for x in self.app_critiques}
|
||||||
if with_app_critiques
|
if with_app_critiques
|
||||||
else {},
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_dict_bul(self):
|
def to_dict_bul(self):
|
||||||
@ -464,9 +562,9 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if competence is None:
|
if competence is None:
|
||||||
parcour_niveaux: list[
|
parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
|
||||||
ApcParcoursNiveauCompetence
|
annee_parcour.niveaux_competences
|
||||||
] = annee_parcour.niveaux_competences
|
)
|
||||||
niveaux: list[ApcNiveau] = [
|
niveaux: list[ApcNiveau] = [
|
||||||
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
|
||||||
for pn in parcour_niveaux
|
for pn in parcour_niveaux
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""ScoDoc 9 models : Formations
|
"""ScoDoc 9 models : Formations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from flask import abort, g
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
import app
|
import app
|
||||||
@ -64,6 +66,21 @@ class Formation(db.Model):
|
|||||||
"titre complet pour affichage"
|
"titre complet pour affichage"
|
||||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
|
||||||
|
"""Formation ou 404, cherche uniquement dans le département spécifié
|
||||||
|
ou le courant (g.scodoc_dept)"""
|
||||||
|
if not isinstance(formation_id, int):
|
||||||
|
try:
|
||||||
|
formation_id = int(formation_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
abort(404, "formation_id invalide")
|
||||||
|
if g.scodoc_dept:
|
||||||
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||||
|
if dept_id is not None:
|
||||||
|
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
|
||||||
|
return cls.query.filter_by(id=formation_id).first_or_404()
|
||||||
|
|
||||||
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
def to_dict(self, with_refcomp_attrs=False, with_departement=True):
|
||||||
"""As a dict.
|
"""As a dict.
|
||||||
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.
|
||||||
|
58
app/templates/but/change_refcomp.j2
Normal file
58
app/templates/but/change_refcomp.j2
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{# -*- mode: jinja-html -*- #}
|
||||||
|
{% extends "base.j2" %}
|
||||||
|
{% import 'wtf.j2' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<h1>Changer le référentiel de compétences de la formation
|
||||||
|
{{formation.get_titre_version()}}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="help">
|
||||||
|
|
||||||
|
<p> Normalement, il n'est pas possible de changer une
|
||||||
|
formation de référentiel de compétences. En effet, de nombreux éléments sont
|
||||||
|
déduis du référentiel: les parcours, les compétences, les apprentissages
|
||||||
|
critiques, et donc les validations de jury des étudiants qui ont suivi la
|
||||||
|
formation.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p> Cependant, dans certains cas, le ministère a publié une nouvelle version
|
||||||
|
d'un référentiel de spécialité, mais les changements ne sont que très légers:
|
||||||
|
détails des noms de parcours ou des compétences. Dans ces cas là, la structure
|
||||||
|
étant la même, il est autorisé de changer le référentiel.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Seuls les référentiels déjà chargés et compatibles (ayant la même structure)
|
||||||
|
sont proposés dans le menu ci-dessous.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="fontred">
|
||||||
|
Attention: tout changement de référentiel entraine la perte des associations
|
||||||
|
entre les modules et les apprentissages critiques.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="fontred">
|
||||||
|
Attention: <b>fonction expérimentale</b>. Vérifiez vos sauvegardes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>La formation est actuellement associée au référentiel
|
||||||
|
<a class="stdlink" href="{{
|
||||||
|
url_for('notes.refcomp_show',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
refcomp_id=formation.referentiel_competence.id)
|
||||||
|
}}">{{formation.referentiel_competence.get_title()}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div>
|
||||||
|
{{ form.object_select.label }}<br>
|
||||||
|
{{ form.object_select }}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 24px;">{{ form.submit() }} {{ form.cancel() }}</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -27,9 +27,18 @@
|
|||||||
<li>Formations se référant à ce référentiel:
|
<li>Formations se référant à ce référentiel:
|
||||||
<ul>
|
<ul>
|
||||||
{% for formation in ref.formations %}
|
{% for formation in ref.formations %}
|
||||||
<li><a class="stdlink" href="{{
|
<li>
|
||||||
|
<a class="stdlink" href="{{
|
||||||
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
||||||
}}">{{ formation.get_titre_version() }}</a></li>
|
}}">{{ formation.get_titre_version() }}</a>
|
||||||
|
{% if referentiels_equivalents %}
|
||||||
|
<a style="margin-left: 8px;" class="stdlink" href="
|
||||||
|
{{ url_for('notes.formation_change_refcomp',
|
||||||
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
||||||
|
}}
|
||||||
|
">(changer)</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><em>aucune</em></li>
|
<li><em>aucune</em></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -3,6 +3,7 @@ PN / Référentiel de compétences
|
|||||||
|
|
||||||
Emmanuel Viennet, 2021
|
Emmanuel Viennet, 2021
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -19,11 +20,20 @@ from app import db, log
|
|||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Formation
|
from app.models import Formation
|
||||||
from app.models.but_refcomp import ApcReferentielCompetences
|
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.import_refcomp import orebut_import_refcomp
|
||||||
from app.but.forms.refcomp_forms import FormationRefCompForm, RefCompLoadForm
|
from app.but.forms.refcomp_forms import (
|
||||||
|
FormationChangeRefCompForm,
|
||||||
|
FormationRefCompForm,
|
||||||
|
RefCompLoadForm,
|
||||||
|
)
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
|
from app.scodoc.sco_exceptions import (
|
||||||
|
ScoFormatError,
|
||||||
|
ScoNoReferentielCompetences,
|
||||||
|
ScoValueError,
|
||||||
|
)
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.views import notes_bp as bp
|
from app.views import notes_bp as bp
|
||||||
from app.views import ScoData
|
from app.views import ScoData
|
||||||
@ -47,9 +57,12 @@ def refcomp(refcomp_id):
|
|||||||
def refcomp_show(refcomp_id):
|
def refcomp_show(refcomp_id):
|
||||||
"""Affichage du référentiel de compétences."""
|
"""Affichage du référentiel de compétences."""
|
||||||
referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id)
|
referentiel_competence = 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(
|
return render_template(
|
||||||
"but/refcomp_show.j2",
|
"but/refcomp_show.j2",
|
||||||
ref=referentiel_competence,
|
ref=referentiel_competence,
|
||||||
|
referentiels_equivalents=referentiels_equivalents,
|
||||||
title="Référentiel de compétences",
|
title="Référentiel de compétences",
|
||||||
data_source=url_for(
|
data_source=url_for(
|
||||||
"notes.refcomp",
|
"notes.refcomp",
|
||||||
@ -279,3 +292,55 @@ def refcomp_load(formation_id=None):
|
|||||||
formation=formation,
|
formation=formation,
|
||||||
title="Chargement réf. compétences",
|
title="Chargement réf. compétences",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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",
|
||||||
|
)
|
||||||
|
13
ressources/referentiels/equivalences.yaml
Normal file
13
ressources/referentiels/equivalences.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Certains référentiels de compétences peuvent être considérés
|
||||||
|
# comme équivalents
|
||||||
|
# Si ils ont la même structure
|
||||||
|
# Voir ApcReferentielCompetences.map_to_other_referentiel
|
||||||
|
|
||||||
|
# Mappings: nouveau : ancien
|
||||||
|
QLIO: # la clé est 'specialite'
|
||||||
|
parcours: # codes de parcours
|
||||||
|
OSC: MSC
|
||||||
|
QMI: MQSE
|
||||||
|
# competences: # titres de compétences ('nom_court' dans le XML)
|
||||||
|
|
||||||
|
SD: STID
|
Loading…
Reference in New Issue
Block a user