chargement/association ref. comp. BUT

This commit is contained in:
Emmanuel Viennet 2021-12-03 11:03:33 +01:00
parent e88e280994
commit 4d857a1567
11 changed files with 214 additions and 26 deletions

View File

@ -0,0 +1,35 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 : Formulaires / référentiel de compétence
"""
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms import SelectField, SubmitField
class FormationRefCompForm(FlaskForm):
referentiel_competence = SelectField("Référentiels déjà chargés")
submit = SubmitField("Valider")
cancel = SubmitField("Annuler")
class RefCompLoadForm(FlaskForm):
upload = FileField(
label="Sélectionner un fichier XML Orébut",
validators=[
FileRequired(),
FileAllowed(
[
"xml",
],
"Fichier XML Orébut seulement",
),
],
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler")

View File

@ -17,14 +17,15 @@ from app.models.but_refcomp import (
from app.scodoc.sco_exceptions import FormatError
def orebut_import_refcomp(xml_file: TextIO):
def orebut_import_refcomp(xml_file: TextIO, dept_id: int, orig_filename=None):
tree = ElementTree.parse(xml_file)
root = tree.getroot()
if root.tag != "referentiel_competence":
raise FormatError("élément racine 'referentiel_competence' manquant")
ref = ApcReferentielCompetences(
**ApcReferentielCompetences.attr_from_xml(root.attrib)
)
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
args["dept_id"] = dept_id
args["scodoc_orig_filename"] = orig_filename
ref = ApcReferentielCompetences(**args)
db.session.add(ref)
competences = root.find("competences")
if not competences:
@ -99,7 +100,7 @@ competence = competences.findall("competence")[0] # XXX
from app.but.import_refcomp import *
f = open("but-RT-refcomp-30112021.xml")
ref = orebut_import_refcomp(f)
ref = orebut_import_refcomp(f, 0)
#------
from app.but.import_refcomp import *
ref = ApcReferentielCompetences.query.first()

View File

@ -1,5 +1,6 @@
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
"""
from datetime import datetime
from enum import unique
from typing import Any
@ -31,9 +32,13 @@ class ApcReferentielCompetences(db.Model, XMLModel):
specialite = db.Column(db.Text())
specialite_long = db.Column(db.Text())
type_titre = db.Column(db.Text())
_xml_attribs = { # xml_attrib : attribute
_xml_attribs = { # Orébut xml attrib : attribute
"type": "type_titre",
}
# ScoDoc specific fields:
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
scodoc_orig_filename = db.Column(db.Text())
# Relations:
competences = db.relationship(
"ApcCompetence",
backref="referentiel",
@ -56,17 +61,26 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"specialite": self.specialite,
"specialite_long": self.specialite_long,
"type_titre": self.type_titre,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z",
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences},
"parcours": {x.code: x.to_dict() for x in self.parcours},
}
class ApcCompetence(db.Model, XMLModel):
__table_args__ = (
# les compétences dans Orébut sont identifiées par leur "titre"
# unique au sein d'un référentiel:
db.UniqueConstraint(
"referentiel_id", "titre", name="apc_competence_referentiel_id_titre_key"
),
)
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
titre = db.Column(db.Text(), nullable=False)
titre = db.Column(db.Text(), nullable=False, index=True)
titre_long = db.Column(db.Text())
couleur = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
@ -158,7 +172,7 @@ class ApcAppCritique(db.Model, XMLModel):
"Apprentissage Critique BUT"
id = db.Column(db.Integer, primary_key=True)
niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False)
code = db.Column(db.Text(), nullable=False)
code = db.Column(db.Text(), nullable=False, index=True)
libelle = db.Column(db.Text())
modules = db.relationship(
@ -175,14 +189,14 @@ class ApcAppCritique(db.Model, XMLModel):
return self.code + " - " + self.titre
def __repr__(self):
return "<AC {}>".format(self.code)
return "<AppCritique {}>".format(self.code)
def get_saes(self):
"""Liste des SAE associées"""
return [m for m in self.modules if m.module_type == ModuleType.SAE]
ApcModulesACs = db.Table(
ApcAppCritiqueModules = db.Table(
"apc_modules_acs",
db.Column("module_id", db.ForeignKey("notes_modules.id")),
db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")),

View File

@ -39,7 +39,9 @@ class Formation(db.Model):
referentiel_competence_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
)
referentiel_competence = db.relationship( # one-to-one
"ApcReferentielCompetences", backref="formation", uselist=False
)
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")

View File

@ -252,6 +252,7 @@ class TypeParcours(object):
UE_TYPE_NAME.keys()
) # par defaut, autorise tous les types d'UE
APC_SAE = False # Approche par compétences avec ressources et SAÉs
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
def check(self, formation=None):
return True, "" # status, diagnostic_message
@ -307,6 +308,7 @@ class ParcoursBUT(TypeParcours):
NB_SEM = 6
COMPENSATION_UE = False
APC_SAE = True
USE_REFERENTIEL_COMPETENCES = True
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]

View File

@ -575,12 +575,23 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</div>
"""
)
if formation.referentiel_competence is None:
descr_refcomp = ""
msg_refcomp = "associer à un référentiel de compétences"
else:
descr_refcomp = f"{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}"
msg_refcomp = "changer"
H.append(
f"""
<ul>
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc',
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}">{msg_refcomp}</a>
</li>
<li><a class="stdlink" href="{
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
}">éditer les coefficients des ressources et SAÉs</a></li>
}">éditer les coefficients des ressources et SAÉs</a>
</li>
</ul>
"""
)

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Associer un référentiel de compétences</h1>
<div class="help">
Association d'un référentiel de compétence à la formation
{{formation.titre}} ({{formation.acronyme}})
</div>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
<div class="help">
Pour charger un nouveau référentiel de compétences Orébut,
<a href="{{url_for(
'notes.refcomp_load', scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}}">passer par cette page</a>.
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Charger un référentiel de compétences</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -4,12 +4,13 @@ PN / Référentiel de compétences
Emmanuel Viennet, 2021
"""
from flask import url_for
from flask import url_for, flash
from flask import jsonify
from flask import current_app, g, request
from flask.templating import render_template
from flask_login import current_user
from werkzeug.utils import redirect
from werkzeug.utils import secure_filename
from config import Config
@ -17,14 +18,88 @@ from app import db
from app import models
from app.decorators import scodoc, permission_required
from app.models.formations import Formation
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.sco_permissions import Permission
from app.views import notes_bp as bp
@bp.route("/pn/comp/<int:refcomp_id>")
@bp.route("/referentiel/comp/<int:refcomp_id>")
@scodoc
@permission_required(Permission.ScoView)
def refcomp(refcomp_id):
ref = ApcReferentielCompetences.query.get_or_404(refcomp_id)
return jsonify(ref.to_dict())
@bp.route("/refcomp_assoc/<int:formation_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
def refcomp_assoc(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}")
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 (
ApcReferentielCompetences.query.get(referentiel_competence_id) is not None
)
formation.referentiel_competence_id = referentiel_competence_id
db.session.add(formation)
db.session.commit()
flash("nouveau référentiel de compétences associé")
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
)
return render_template(
"but/refcomp_assoc.html",
form=form,
referentiel_competence_id=formation.referentiel_competence_id,
formation=formation,
)
@bp.route("/refcomp_load/<int:formation_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
def refcomp_load(formation_id=None):
"""Formulaire association ref. compétence"""
if formation_id is not None:
formation = Formation.query.get_or_404(formation_id)
else:
formation = None
form = RefCompLoadForm()
if form.validate_on_submit():
f = form.upload.data
filename = secure_filename(f.filename)
ref = orebut_import_refcomp(f, dept_id=g.scodoc_dept_id, orig_filename=filename)
if formation is not None:
return redirect(
url_for(
"notes.refcomp_assoc",
scodoc_dept=g.scodoc_dept,
formation_id=formation.formation_id,
)
)
else:
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
return render_template(
"but/refcomp_load.html",
form=form,
formation=formation,
)

View File

@ -1,8 +1,8 @@
"""refcomp
"""refcomp index
Revision ID: eed6d50fd9cb
Revision ID: 92789d50f6b6
Revises: 00ad500fb118
Create Date: 2021-12-02 17:34:10.999132
Create Date: 2021-12-03 10:56:43.921559
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'eed6d50fd9cb'
revision = '92789d50f6b6'
down_revision = '00ad500fb118'
branch_labels = None
depends_on = None
@ -18,27 +18,38 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('app_crit')
op.drop_table('modules_acs')
op.drop_table('app_crit')
op.add_column('apc_annee_parcours', sa.Column('ordre', sa.Integer(), nullable=True))
op.drop_column('apc_annee_parcours', 'numero')
op.create_index(op.f('ix_apc_app_critique_code'), 'apc_app_critique', ['code'], unique=False)
op.create_unique_constraint('apc_competence_referentiel_id_titre_key', 'apc_competence', ['referentiel_id', 'titre'])
op.create_index(op.f('ix_apc_competence_titre'), 'apc_competence', ['titre'], unique=False)
op.add_column('apc_referentiel_competences', sa.Column('scodoc_date_loaded', sa.DateTime(), nullable=True))
op.add_column('apc_referentiel_competences', sa.Column('scodoc_orig_filename', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('apc_referentiel_competences', 'scodoc_orig_filename')
op.drop_column('apc_referentiel_competences', 'scodoc_date_loaded')
op.drop_index(op.f('ix_apc_competence_titre'), table_name='apc_competence')
op.drop_constraint('apc_competence_referentiel_id_titre_key', 'apc_competence', type_='unique')
op.drop_index(op.f('ix_apc_app_critique_code'), table_name='apc_app_critique')
op.add_column('apc_annee_parcours', sa.Column('numero', sa.INTEGER(), autoincrement=False, nullable=True))
op.drop_column('apc_annee_parcours', 'ordre')
op.create_table('app_crit',
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('app_crit_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('code', sa.TEXT(), autoincrement=False, nullable=False),
sa.Column('titre', sa.TEXT(), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('id', name='app_crit_pkey'),
postgresql_ignore_search_path=False
)
op.create_table('modules_acs',
sa.Column('module_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('ac_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['ac_id'], ['app_crit.id'], name='modules_acs_ac_id_fkey'),
sa.ForeignKeyConstraint(['module_id'], ['notes_modules.id'], name='modules_acs_module_id_fkey')
)
op.create_table('app_crit',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('code', sa.TEXT(), autoincrement=False, nullable=False),
sa.Column('titre', sa.TEXT(), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('id', name='app_crit_pkey')
)
# ### end Alembic commands ###

View File

@ -44,7 +44,7 @@ ref_xml = """<?xml version="1.0" encoding="UTF-8"?>
def test_but_refcomp(test_client):
"""modèles ref. comp."""
f = io.StringIO(ref_xml)
ref = orebut_import_refcomp(f)
ref = orebut_import_refcomp(0, f)
assert ref.references.count() == 2
assert ref.competences[0].situations.count() == 2
assert ref.competences[0].situations[0].libelle.startswith("Conception ")